Home > Enterprise >  LiveData Observer isn't triggered for the second time
LiveData Observer isn't triggered for the second time

Time:06-09

I'm expecting that the observer will be triggered when I'm hitting API by clicking one of the side menu. When I clicked one of the menu, Retrofit actually gave me the response with the correct value. The problem is, the Observer isn't getting triggered for the second time. I've trace the problem and find out that my Repository isn't returning a value even though my Retrofit already update the MutableLiveData.

Here's the RemoteDataSource

override fun getDisastersByFilter(filter: String?): LiveData<ApiResponse<DisastersDTO?>> {
    val result = MutableLiveData<ApiResponse<DisastersDTO?>>()

    apiService.getDisastersByFilter(filter).enqueue(object : Callback<DisastersResponse> {
        override fun onResponse(
            call: Call<DisastersResponse>,
            response: Response<DisastersResponse>
        ) {
            if(response.isSuccessful) {
                val data = response.body()
                data?.disastersDTO?.let {
                    result.postValue(ApiResponse.Success(it))
                    Log.d("RemoteDataSource", "$it")
                } ?: run {
                    result.postValue(ApiResponse.Error("Bencana alam tidak ditemukan"))
                }
            } else {
                result.postValue(ApiResponse.Error("Terjadi kesalahan!"))
            }
        }

        override fun onFailure(call: Call<DisastersResponse>, t: Throwable) {
            result.postValue(ApiResponse.Error(t.localizedMessage!!))
            Log.d("RemoteDataSource", t.localizedMessage!!)
        }

    })

    return result
}

Here's the Repository

override fun getDisastersByFilter(filter: String?): LiveData<Resource<List<Disaster>>> =
    remoteDataSource.getDisastersByFilter(filter).map {
        when (it) {
            is ApiResponse.Empty -> Resource.Error("Terjadi error")
            is ApiResponse.Error -> Resource.Error(it.errorMessage)
            is ApiResponse.Loading -> Resource.Loading()
            is ApiResponse.Success -> Resource.Success(
                DataMapper.disastersResponseToDisasterDomain(
                    it.data
                )
            )
        }
    }

Here's the SharedViewModel

fun getDisastersByFilter(filter: String? = "gempa"): LiveData<Resource<List<Disaster>>> =
    useCase.getDisastersByFilter(filter)

Here's the MapsFragment

private val viewModel: SharedViewModel by activityViewModels()
viewModel.getDisastersByFilter("gempa").observe(viewLifecycleOwner) {
        when (it) {
            is Resource.Success -> {
                Log.d("MapsFragmentFilter", "${it.data}")
                it.data?.let { listDisaster ->
                    if(listDisaster.isNotEmpty()) {
                        map.clear()
                        addGeofence(listDisaster)
                        listDisaster.map { disaster ->
                            placeMarker(disaster)
                            addCircle(disaster)
                        }
                    }
                }
            }

            is Resource.Error -> Toast.makeText(context, "Filter Error", Toast.LENGTH_SHORT).show()

            is Resource.Loading -> {}
        }
    }

Here's the MainActivity that triggers the function to hit API

private val viewModel: SharedViewModel by viewModels()
binding.navViewMaps.setNavigationItemSelectedListener { menu ->
        when (menu.itemId) {
            R.id.filter_gempa -> viewModel.getDisastersByFilter("gempa")
            R.id.filter_banjir -> viewModel.getDisastersByFilter("banjir")
            R.id.about_us -> viewModel.getDisasters()
        }

        binding.drawerLayoutMain.closeDrawers()

        true
    }

CodePudding user response:

I can't be sure from what you've posted, but your menu options call getDisastersByFilter on your SharedViewModel, and it looks like that eventually calls through to getDisastersByFilter in RemoteDataSource.

That function creates a new LiveData and returns it, and all your other functions (including the one in viewModel) just return that new LiveData. So if you want to see the result that's eventually posted to it, you need to observe that new one.

I don't know where the fragment code you posted is from, but it looks like you're just calling and observing viewModel.getDisastersByFilter once. So when that first happens, it does the data fetch and you get a result on the LiveData it returned. That LiveData won't receive any more results, from the looks of your code - it's a one-time, disposable thing that receives a result later, and then it's useless.


If I've got that right, you need to rework how you're handling your LiveDatas. The fragment needs to get the result of every viewModel.getDisastersByFilter call, so it can observe the result - it might be better if your activity passes an event to the fragment ("this item was clicked") and the fragment handles calling the VM, and it can observe the result while it's at it (pass it to a function that wires that up so you don't have to keep repeating your observer code)

The other approach would be to have the Fragment observe a currentData livedata, that's wired up to show the value of a different source livedata. Then when you call getDisastersByFilter, that source livedata is swapped for the new one. The currentData one gets any new values posted to this new source, and the fragment only has to observe that single LiveData once. All the data gets piped into it by the VM.

I don't have time to do an example, but have a look at this Transformations stuff (this is one of the developers' blogs): https://medium.com/androiddevelopers/livedata-beyond-the-viewmodel-reactive-patterns-using-transformations-and-mediatorlivedata-fda520ba00b7

CodePudding user response:

What I believe you are doing wrong is using LiveData in the first place while using a retrofit.

You are getting a response asynchronously while your code is running synchronously. So, you need to make use of suspending functions by using suspend.

And while calling this function from ViewModel, wrap it with viewModelScope.launch{}

fun getDisastersByFilter(filter: String? = "gempa") = viewModelScope.launch {
    useCase.getDisastersByFilter(filter).collect{
  // do something....
  // assign the values to MutableLiveData or MutableStateFlows
  }
}

You should either be using RxJava or CallbackFlow.

I prefer Flows, given below is an example of how your code might look if you use callback flow.

    suspend fun getDisastersByFilter(filter: String?): Flow<ApiResponse<DisastersDTO?>> =
        callbackFlow {

            apiService.getDisastersByFilter(filter)
                .enqueue(object : Callback<DisastersResponse> {
                    override fun onResponse(
                        call: Call<DisastersResponse>,
                        response: Response<DisastersResponse>
                    ) {
                        if (response.isSuccessful) {
                            val data = response.body()
                            data?.disastersDTO?.let {
                                trySend(ApiResponse.Success(it))
//                                result.postValue(ApiResponse.Success(it))
                                Log.d("RemoteDataSource", "$it")
                            } ?: run {
                                trySend(ApiResponse.Error("Bencana alam tidak ditemukan"))
//                                result.postValue(ApiResponse.Error("Bencana alam tidak ditemukan"))
                            }
                        } else {
                            trySend(ApiResponse.Error("Terjadi kesalahan!"))
//                            result.postValue(ApiResponse.Error("Terjadi kesalahan!"))
                        }
                    }

                    override fun onFailure(call: Call<DisastersResponse>, t: Throwable) {
                        trySend(ApiResponse.Error(t.localizedMessage!!))
//                        result.postValue(ApiResponse.Error(t.localizedMessage!!))
                        Log.d("RemoteDataSource", t.localizedMessage!!)
                    }

                })
            awaitClose()
        }
  • Related