I have the following error wrapper in my application -
open class ResponseHandler {
fun <T : Any> handleSuccess(data: T): Resource<T> {
return Resource.success(data)
}
fun <T : Any> handleException(e: Exception): Resource<T> {
return when (e) {
is HttpException -> Resource.error(getErrorMessage(e.code()), null)
else -> Resource.error(getErrorMessage(Int.MAX_VALUE), null)
}
}
private fun getErrorMessage(code: Int): String {
return when (code) {
401 -> ERROR401
404 -> ERROR404
else -> GENERAL_ERROR
}
}
/**
* Wrapper class that enables error / loading / success handling.
*/
data class Resource<out T>(val status: Status, val data: T?, val message: String?) {
companion object {
fun <T> success(data: T?): Resource<T> {
return Resource(Status.SUCCESS, data, null)
}
fun <T> error(message: String, data: T?): Resource<T> {
return Resource(Status.ERROR, data, message)
}
fun <T> loading(data: T?): Resource<T> {
return Resource(Status.LOADING, data, null)
}
}
enum class Status {
SUCCESS,
ERROR,
LOADING
}
}
}
and when using Retrofit + Coroutines, the use it quite straight forward -
suspend fun getSomethingFromPath(): ResponseHandler.Resource<Any> {
return try {
responseHandler.handleSuccess(networkApi.getSomethingFromPath())
} catch (e : Exception) {
responseHandler.handleException(e)
}
}
But when I want to get data from my Firestore collection, I am having trouble implementing the method -
fun getAllApplicationActivities(groupsList: List<String>): ResponseHandler.Resource<ActivityCollectionModel> {
Firebase.firestore.collection(ACTIVITIES_COLLECTION)
.whereIn(GROUP_ID, groupsList)
.get()
.addOnSuccessListener { documents ->
val activitiesList = documents.toObjects(ActivityCollectionModel::class.java)
//Now what?
}
.addOnSuccessListener { exception ->
}
}
How can I make the function return a ResponseHandler.Resource<ActivityCollectionModel> from the addOnSuccessListener and addOnSuccessListener callbacks?
Use suspendCoroutine or suspendCancellableCoroutine
suspend fun getAllApplicationActivities(groupsList: List<String>) = suspendCoroutine { cont ->
Firebase.firestore.collection(ACTIVITIES_COLLECTION)
.whereIn(GROUP_ID, groupsList)
.get()
.addOnSuccessListener { documents ->
val activitiesList = documents.toObjects(ActivityCollectionModel::class.java)
cont.resume(activitiesList)
}
.addOnFailureListener { exception ->
cont.resumeWithException(exception)
}
}
Related
I have this observer for my viewmodel so that I can setup my adapter but, when I run the app, it gives me the NullPointerException error on this line:
japaneseAdapter = it.data?.let { it1 -> JapaneseAdapter(it1) }!!
This is the activity with that line:
#AndroidEntryPoint
class JapaneseActivity : AppCompatActivity() {
private lateinit var binding: ActivityJapaneseBinding
private val japaneseViewModel by viewModels<JapaneseViewModel>()
private lateinit var japaneseAdapter: JapaneseAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityJapaneseBinding.inflate(layoutInflater)
setContentView(binding.root)
japaneseViewModel.japaneseResponse.observe(this, {
when(it.status){
Resource.Status.LOADING -> { }
Resource.Status.SUCCESS -> {
japaneseAdapter = it.data?.let { it1 -> JapaneseAdapter(it1) }!!
binding.rvNews.adapter = japaneseAdapter
}
Resource.Status.ERROR -> { Log.d("ERROR","ERROR RAISED") }
}
})
}
}
This is the adapter:
class JapaneseAdapter(private var japaneseResponse: List<JapaneseResponse>) :
RecyclerView.Adapter<JapaneseAdapter.ViewHolder>() {
inner class ViewHolder(
view: View
) : RecyclerView.ViewHolder(view) {
private val binding = NewsItemsBinding.bind(view)
private val itemTitle: TextView = binding.tvTitle
private val itemImage: ImageView = binding.ivNews
private val itemDescription: TextView = binding.tvDescription
fun bind(response: JapaneseResponse) {
Picasso.get().load(response.urlToImage).into(itemImage)
itemTitle.text = response.Title
itemDescription.text = response.Description
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val v = LayoutInflater.from(parent.context).inflate(R.layout.news_items, parent, false)
return ViewHolder(v)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(japaneseResponse[position])
}
override fun getItemCount(): Int {
return japaneseResponse.size
}
}
Generic data source:
abstract class BaseDataSource {
protected suspend fun <T> getResult(call: suspend () -> Response<ApiResponse<T>>): Resource<T> {
try {
val response = call()
// if(response.isSuccessful) {
// val body = response.body()?.data
// if(body != null) return Resource.success(body)
// }
val body = response.body()?.data
return Resource.success(body)
//return Resource.error("${response.code()}: ${response.message()}")
} catch (e: Exception) {
return Resource.error(e.message ?: "Generic error")
}
}
}
data class Resource<out T>(val status: Status, val data: T?, val message: String?) : Serializable {
enum class Status {
SUCCESS,
ERROR,
LOADING
}
companion object {
fun <T> success(data: T?): Resource<T> {
return Resource(
Status.SUCCESS,
data,
null
)
}
fun <T> error(message: String, data: T? = null): Resource<T> {
return Resource(
Status.ERROR,
data,
message
)
}
fun <T> loading(data: T? = null): Resource<T> {
return Resource(
Status.LOADING,
data,
null
)
}
}
fun isSuccessful() = status == Status.SUCCESS
fun isError() = status == Status.ERROR
fun isLoading() = status == Status.LOADING
}
The data source for the Japanese news:
class JapaneseDataSource #Inject constructor(private val japaneseService: JapaneseService) :
BaseDataSource() {
suspend fun getJpNews() = getResult { japaneseService.jpNews() }
}
Repository:
class JapaneseRepository #Inject constructor(
private val remote: JapaneseDataSource
) {
suspend fun jpNews() =
remote.getJpNews()
}
The service:
interface JapaneseService {
#GET("/v2/top-headlines?country=jp&apiKey=77acc490875643c5b2328fb615e0cf83")
suspend fun jpNews(): Response<ApiResponse<List<JapaneseResponse>>>
}
I can see that the response is there since I have okhttp logging it for me but for some reason it seems to be null and I am not sure why...
Any help?
japaneseAdapter = it.data?.let { it1 -> JapaneseAdapter(it1) }!!
!! is Kotlin's "crash the app" operator. It says that you want to crash the app if the value you are applying !! to is null. Your objective, as a Kotlin programmer, is to avoid using !!.
In this case, the fact that you are crashing on that line with that error means that !! is being applied to null. That will occur if it.data evaluates to null.
it.data appears to be a Resource object with a status of SUCCESS. So, presumably, you are calling success() with a value of null.
With all that in mind, you will need to use your debugger and see why val body = response.body()?.data is evaluating to null, or see where else you are getting a Resource with null data.
And, please, try to avoid using !!.
I am following the "best practices"(?) from the GitHub sample: https://github.com/android/architecture-components-samples/blob/master/GithubBrowserSample/app/src/main/java/com/android/example/github/repository/NetworkBoundResource.kt
I am getting an error
java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to com.xxx.yyy.data.model.TestUser
I've tried using Gson to convert to JSON and back to the Generic RequestType, but that doesn't work either. The data is coming back just fine. If I cast the response in the NetworkBoundResource, then it works - but that kind of defeats the purpose of generics. I am also using the aws-android-sdk to invoke lambda, which makes things a HUGE pain, but that's AWS for ya
TestUser
public class TestUser {
private String username;
public TestUser() {
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
NetworkBoundResource
abstract class NetworkBoundResource<ResultType, RequestType>
#MainThread constructor(private val appExecutors: AppExecutors) {
private val result = MediatorLiveData<Resource<ResultType>>()
init {
result.value = Resource.loading(null)
val dbSource = loadFromDb()
result.addSource(dbSource) { data ->
result.removeSource(dbSource)
if (shouldFetch(data)) {
Log.e("NetworkBoundResource", "fetching from network")
appExecutors.networkIO().execute {
fetchFromNetwork(dbSource)
}
} else {
Log.e("NetworkBoundResource", "not fetching from network")
result.addSource(dbSource) { newData ->
setValue(Resource.success(newData))
}
}
}
}
#MainThread
private fun setValue(newValue: Resource<ResultType>) {
if (result.value != newValue) {
result.value = newValue
}
}
private fun fetchFromNetwork(dbSource: LiveData<ResultType>) {
val apiResponse = MutableLiveData<ApiResponse<RequestType>>()
// This is super dumb, but can't createCall() on the main thread 🤦
var lambdaResponse: LambdaResponse<RequestType>? = null
var exception: Exception? = null
try {
lambdaResponse = createCall()
} catch (e: Exception) {
exception = e
}
appExecutors.mainThread().execute {
// Can't setValue on a background thread 🤦
if (exception != null) {
apiResponse.value = ApiResponse.create(exception)
} else if (lambdaResponse != null) {
apiResponse.value = ApiResponse.create(lambdaResponse)
}
// we re-attach dbSource as a new source, it will dispatch its latest value quickly
result.addSource(dbSource) { newData ->
setValue(Resource.loading(newData))
}
result.addSource(apiResponse) { response ->
result.removeSource(apiResponse)
result.removeSource(dbSource)
when (response) {
is ApiSuccessResponse -> {
appExecutors.diskIO().execute {
val x = processResponse(response)
// FAILING HERE
saveCallResult(x)
appExecutors.mainThread().execute {
// we specially request a new live data,
// otherwise we will get immediately last cached value,
// which may not be updated with latest results received from network.
result.addSource(loadFromDb()) { newData ->
setValue(Resource.success(newData))
}
}
}
}
is ApiEmptyResponse -> {
appExecutors.mainThread().execute {
// reload from disk whatever we had
result.addSource(loadFromDb()) { newData ->
setValue(Resource.success(newData))
}
}
}
is ApiErrorResponse -> {
onFetchFailed()
result.addSource(dbSource) { newData ->
setValue(Resource.error(response.errorMessage, newData))
}
}
}
}
}
}
protected open fun onFetchFailed() {
Log.e("NetworkBoundResource", "onFetchFailed")
}
fun asLiveData() = result as LiveData<Resource<ResultType>>
#WorkerThread
protected open fun processResponse(response: ApiSuccessResponse<RequestType>) = response.body
#WorkerThread
protected abstract fun saveCallResult(item: RequestType)
#MainThread
protected abstract fun shouldFetch(data: ResultType?): Boolean
#MainThread
protected abstract fun loadFromDb(): LiveData<ResultType>
#MainThread
protected abstract fun createCall(): LambdaResponse<RequestType>
}
LoginDataSource
fun auth(authRequest: AuthRequest): LiveData<Resource<User>> {
return object : NetworkBoundResource<User, TestUser>(appExecutors) {
override fun saveCallResult(item: TestUser) {
Log.e("username", item.username)
// Log.e("saveCallResult", "saving user to db: ${item.federatedIdentity}")
// userDao.insertAll(item)
}
override fun shouldFetch(data: User?): Boolean {
val fetch = data == null
Log.e("shouldFetch", "$fetch")
return true
}
override fun loadFromDb(): LiveData<User> {
Log.e("loadFromDb", "findById: ${authRequest.federatedIdentity}")
return userDao.findById(authRequest.federatedIdentity)
}
override fun createCall(): LambdaResponse<TestUser> {
Log.e("createCall", "authenticating user: ${authRequest.federatedIdentity}")
return authApiService.auth(authRequest)
}
}.asLiveData()
}
I want to know if it is possible to capture an suspend lambda with generic result with MockK and JUnit5.
I've tried several ways, and in the most recent I'm having a KotlinNullPointerException when I try to run the test.
Here is the code, with the complete class dependencies and test:
class Test {
data class Result<out T>(val status: ResultStatus, val data: T?, val exception: Exception?) {
companion object {
fun <T> success(data: T?): Result<T> {
return Result(ResultStatus.SUCCESS, data, null)
}
fun <T> error(exception: Exception): Result<T> {
return Result(ResultStatus.ERROR, null, exception)
}
fun <T> loading(data: T? = null): Result<T> {
return Result(ResultStatus.LOADING, data, null)
}
}
}
class Dep1() {
fun <R> methodToMock(viewModelScope: CoroutineScope, block: suspend CoroutineScope.() -> R): MutableLiveData<Result<R>> {
val result = MutableLiveData<Result<R>>()
result.value = Result.loading()
viewModelScope.launch {
try {
var asyncRequestResult: R? = null
withContext(Dispatchers.Default) {
asyncRequestResult = block()
}
result.value = Result.success(asyncRequestResult)
} catch (cancellationException: CancellationException) {
} catch (exception: Exception) {
result.value = Result.error(exception)
}
}
return result
}
}
class Dep2() {
fun methodToAssert(): Boolean {
println("Called")
return true
}
}
class ClassToTest(private val dep1: Dep1, private val dep2: Dep2) {
fun methodToCall(coroutineScope: CoroutineScope): MutableLiveData<Result<Boolean>> {
return dep1.methodToMock(coroutineScope) {
dep2.methodToAssert()
}
}
}
private val dep1: Dep1 = mockk()
private val dep2: Dep2 = mockk(relaxed = true)
private val mViewModelScope: CoroutineScope = GlobalScope
#Test
fun `Check if is calling the required methods correctly`() {
val classToTest = ClassToTest(dep1, dep2)
val transactionLambda = slot<suspend CoroutineScope.() -> Boolean>()
coEvery { dep1.methodToMock(mViewModelScope, capture(transactionLambda)) } coAnswers {
MutableLiveData(Result.success(transactionLambda.captured.invoke(mViewModelScope)))
}
classToTest.methodToCall(mViewModelScope)
verify { dep2.methodToAssert() }
}
}
If anyone is also having this problem, I was able to solve it using every instead of coEvery and calling the coInvoke() slot method:
every { dep1.methodToMock(mViewModelScope, capture(transactionLambda)) } answers {
MutableLiveData(Result.success(transactionLambda.coInvoke(mViewModelScope)))
}
I was spending 3 hours and I couldn't find the parsing error reason.
I have this JSON
[
{
"id": "WMWSW31030T222518",
"modelName": "MINI",
"name": "Vanessa",
"make": "BMW",
"latitude": 48.134557,
"longitude": 11.576921,
"carImageUrl": "https://cdn.sixt.io/codingtask/images/mini.png"
},
{
"id": "WMWSU31070T077232",
"modelName": "MINI",
"name": "Regine",
"make": "BMW",
"latitude": 48.114988,
"longitude": 11.598359,
"carImageUrl": "https://cdn.sixt.io/codingtask/images/mini.png"
}
]
After parsing I got this error
An error happened: com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_OBJECT but was BEGIN_ARRAY at line 1 column 2
Here is my code
--Repository--
interface CarRepository {
suspend fun getCars(): LiveData<Resource<List<Car>>>
}
class CarRepositoryImpl(private val datasource: CarDatasource,
private val dao: CarDao): CarRepository{
override suspend fun getCars(): LiveData<Resource<List<Car>>> {
return object : NetworkBoundResource<List<Car>, ApiResult<Car>>() {
override fun processResponse(response: ApiResult<Car>): List<Car>
= response.items
override suspend fun saveCallResults(items: List<Car>)
= dao.save(items)
override fun shouldFetch(data: List<Car>?): Boolean
= data == null || data.isEmpty()
override suspend fun loadFromDb(): List<Car>
= dao.getCars()
override fun createCallAsync(): Deferred<ApiResult<Car>>
= datasource.fetchCars()
}.build().asLiveData()
}
}
--CarService--
interface CarService {
#GET("cars")
fun fetchCars(): Deferred<ApiResult<Car>>
}
--ApiResult--
data class ApiResult<T>(val items: List<T>)
--NetworkBoundResource--
abstract class NetworkBoundResource<ResultType, RequestType> {
private val result = MutableLiveData<Resource<ResultType>>()
private val supervisorJob = SupervisorJob()
suspend fun build(): NetworkBoundResource<ResultType, RequestType> {
withContext(Dispatchers.Main) { result.value =
Resource.loading(null)
}
CoroutineScope(coroutineContext).launch(supervisorJob) {
val dbResult = loadFromDb()
if (shouldFetch(dbResult)) {
try {
fetchFromNetwork(dbResult)
} catch (e: Exception) {
Log.e("NetworkBoundResource", "An error happened: $e")
setValue(Resource.error(e, loadFromDb()))
}
} else {
Log.d(NetworkBoundResource::class.java.name, "Return data from local database")
setValue(Resource.success(dbResult))
}
}
return this
}
fun asLiveData() = result as LiveData<Resource<ResultType>>
// ---
private suspend fun fetchFromNetwork(dbResult: ResultType) {
Log.d(NetworkBoundResource::class.java.name, "Fetch data from network")
setValue(Resource.loading(dbResult)) // Dispatch latest value quickly (UX purpose)
val apiResponse = createCallAsync().await()
Log.e(NetworkBoundResource::class.java.name, "Data fetched from network")
saveCallResults(processResponse(apiResponse))
setValue(Resource.success(loadFromDb()))
}
#MainThread
private fun setValue(newValue: Resource<ResultType>) {
Log.d(NetworkBoundResource::class.java.name, "Resource: "+newValue)
if (result.value != newValue) result.postValue(newValue)
}
#WorkerThread
protected abstract fun processResponse(response: RequestType): ResultType
#WorkerThread
protected abstract suspend fun saveCallResults(items: ResultType)
#MainThread
protected abstract fun shouldFetch(data: ResultType?): Boolean
#MainThread
protected abstract suspend fun loadFromDb(): ResultType
#MainThread
protected abstract fun createCallAsync(): Deferred<RequestType>
--Resource--
data class Resource<out T>(val status: Status, val data: T?, val error: Throwable?) {
companion object {
fun <T> success(data: T?): Resource<T> {
return Resource(
Status.SUCCESS,
data,
null
)
}
fun <T> error(error: Throwable, data: T?): Resource<T> {
return Resource(
Status.ERROR,
data,
error
)
}
fun <T> loading(data: T?): Resource<T> {
return Resource(
Status.LOADING,
data,
null
)
}
}
enum class Status {
SUCCESS,
ERROR,
LOADING
}
}
Could anyone tell me what was wrong here why the parser failing?
The error is here you should use a list instead ApiResult because ApiResult is an object with a list and GSON tries to parse an object and find a list with atribute name items.
//change to List<Car>
interface CarService {
#GET("cars")
fun fetchCars(): Deferred<List<Car>>
}
I have configLiveData:LiveData<Response<ConfigFile>> where Response is
sealed class Response<out T> {
data class Success<out T>(val data: T) : Response<T>()
data class Failure<out T>(val message: String) : Response<T>()
}
now in ViewModel I want to transform configLiveData to two different
LiveDatas 1.LiveData<ConfigFile> and 2. LiveData<String> and as a result of transformation one of them will be empty.
but I want to get ride of LiveData<Response<ConfigFile>> and have instead of it LiveData<ConfigFile>
override suspend fun fetchConfigFile(): Response<ConfigFile> {
return suspendCoroutine { cont ->
EnigmaRiverContext.getHttpHandler().doHttp(AppConstants.getPath().append("config/appConfig.json").toURL(),
JsonHttpCall("GET"), object : JsonReaderResponseHandler() {
override fun onSuccess(jsonReader: JsonReader) {
try {
val successObject = ApiConfigFile(jsonReader)
cont.resume(Response.Success(successObject.toPresenterModel()))
} catch (e: IOException) {
cont.resume(Response.Failure(e.message))
} catch (e: Exception) {
cont.resume(Response.Failure(e.message ))
}
}
override fun onError(error: Error?) {
cont.resume(Response.Failure("error.message"))
}
})
}
}
It is how my Repository looks like
private fun fetchConfig() {
uiScope.launch {
when (val result = homeRepository.fetchConfigFile()) {
is Response.Success<ConfigFile> -> {
postValue(Response.Success(result.data))
}
is Response.Failure -> postValue(Response.Failure(result.message))
}
}
}
class ConfigFileLiveData #Inject constructor(val homeRepository: IHomeRepository) : LiveData<Response<ConfigFile>>() {
private val liveDataJob = Job()
private val uiScope = CoroutineScope(Dispatchers.Main + liveDataJob)
override fun onActive() {
fetchConfig()
}
private fun fetchConfig() {
viewModelScope.launch {
when (val result = homeRepository.fetchConfigFile()) {
is Response.Success<ConfigFile> -> {
postValue(Response.Success(result.data))
}
is Response.Failure -> postValue(Response.Failure(result.message))
}
}
}
}
I have `ConfigFileLiveData` which is singleton and I want to use this liveData in other viewModels as I need to fetch config once and use it in different ViewModels
class ConfigFileLiveData #Inject constructor(val homeRepository: IHomeRepository) : LiveData<Response<ConfigFile>>() {
override fun onActive() {
fetchConfig()
}
private fun fetchConfig() {
viewModelScope.launch {
when (val result = homeRepository.fetchConfigFile()) {
is Response.Success<ConfigFile> -> {
postValue(Response.Success(result.data))
}
is Response.Failure -> postValue(Response.Failure(result.message))
}
}
}
}
In Viewmodel Define two LiveData variable.
private var configLiveData = MutableLiveData<ConfigFile>()
private var stringLiveData = MutableLiveData<String>()
Modify this method
private fun fetchConfig() {
uiScope.launch {
when (val result = homeRepository.fetchConfigFile()) {
is Response.Success<ConfigFile> -> {
configLiveData.value = Response.Success(result.data)
}
is Response.Failure -> {
stringLiveData.value = Response.Failure(result.message)
}
}
}
}