return value only if Observer method called - kotlin

I'm trying to achieve to return value of method only if Observer method called. But didn't know the right way. I use let but it's required unit and i have to return MutableList<Pair<String,String>>.
That's my method:
override fun getPlaylistsNameAndId(userCategory: String):MutableList<Pair<String,String>> {
val abc = mutableListOf<Pair<String,String>>()
addPlaylistViewModel.getPlaylistsForChips(userCategory).observe(this, Observer { it ->
it.forEach {
abc.add(Pair(it.playlistName,it.playlistId))
}
//i'm called
})
// return if (observer called) else wait for calling.
}

Remember that observe is an async operation. So your method will return immediately after setting up the observer. The observer will only execute later - asynchronously.

I'm doing this in my application: (Pseudo code)
val myData: List<MyData> = arrayListOf()
val myLiveData: LiveData<List<MyData>> by lazy { MyRoomDatabase.getInstance(this).myDataDao().myDataLive }
override fun onCreate() {
super.onCreate()
myLiveData.observe(this, Observer { data ->
myData = data
}
}
Essentially, whenever the live data updates, it'll also update the myData property.
So any time we access the myData property, it should be up to date.

Related

LiveData Observer isn't triggered for the second time

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.
RemoteDataSource.kt
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
}
Repository.kt
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
)
)
}
}
SharedViewModel.kt
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
}
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
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()
}

Change source Flow for LiveData

I try to to use Flow instead of LiveData in repos.
In viewModel:
val state: LiveData<StateModel> = stateRepo
.getStateFlow("euro")
.catch {}
.asLiveData()
Repository:
override fun getStateFlow(currencyCode: String): Flow<StateModel> {
return serieDao.getStateFlow(currencyCode).map {with(stateMapper) { it.fromEntityToDomain() } }
}
It works fine if currCode if always the same during viewModel's lifetime, for example euro
but what to do if currCode is changed to dollar?
How to make state to show a Flow for another param?
You need to switchMap your repository call.
I imagine you could dosomething like this:
class SomeViewModel : ViewModel() {
private val currencyFlow = MutableStateFlow("euro");
val state = currencyFlow.switchMap { currentCurrency ->
// In case they return different types
when (currentCurrency) {
// Assuming all of these database calls return a Flow
"euro" -> someDao.euroCall()
"dollar" -> someDao.dollarCall()
else -> someDao.elseCall()
}
// OR in your case just call
serieDao.getStateFlow(currencyCode).map {
with(stateMapper) { it.fromEntityToDomain() }
}
}
.asLiveData(Dispatchers.IO); //Runs on IO coroutines
fun setCurrency(newCurrency: String) {
// Whenever the currency changes, then the state will emit
// a new value and call the database with the new operation
// based on the neww currency you have selected
currencyFlow.value = newCurrency
}
}

Why does the author wrap tasksRepository.refreshTasks() with viewModelScope.launch?

The following code is from the project.
The function of tasksRepository.refreshTasks() is to insert data from remote server to local DB, it's a time consuming operation.
In class TasksViewModel, asksRepository.refreshTasks() is wrapped with viewModelScope.launch{}, it means launch and careless.
1: How can I guarantee tasksRepository.observeTasks().distinctUntilChanged().switchMap { filterTasks(it) } to return the latest result?
2: I don't know how distinctUntilChanged() work, will it keep listening to return the latest result in whole Lifecycle ?
3: What's happened if I use tasksRepository.observeTasks().switchMap { filterTasks(it) } instead of tasksRepository.observeTasks().distinctUntilChanged().switchMap { filterTasks(it) }
Code
class TasksViewModel(..) : ViewModel() {
private val _items: LiveData<List<Task>> = _forceUpdate.switchMap { forceUpdate ->
if (forceUpdate) {
_dataLoading.value = true
viewModelScope.launch {
tasksRepository.refreshTasks()
_dataLoading.value = false
}
}
tasksRepository.observeTasks().distinctUntilChanged().switchMap { filterTasks(it) }
}
...
}
class DefaultTasksRepository(...) : TasksRepository {
override suspend fun refreshTask(taskId: String) {
updateTaskFromRemoteDataSource(taskId)
}
private suspend fun updateTasksFromRemoteDataSource() {
val remoteTasks = tasksRemoteDataSource.getTasks()
if (remoteTasks is Success) {
tasksLocalDataSource.deleteAllTasks()
remoteTasks.data.forEach { task ->
tasksLocalDataSource.saveTask(task)
}
} else if (remoteTasks is Result.Error) {
throw remoteTasks.exception
}
}
override fun observeTasks(): LiveData<Result<List<Task>>> {
return tasksLocalDataSource.observeTasks()
}
}
switchMap - The returned LiveData delegates to the most recent LiveData created by calling switchMapFunction with the most recent value set to source, without changing the reference. Doc
Yes, it'll keep listening to return the latest result in whole Lifecycle. distinctUntilChanged creates a new LiveData object that does not emit a value until the source LiveData value has been changed. The value is considered changed if equals() yields false.
Yes you can use that too but it'll keep emitting the values even the values are the same as the last emitted value.
e.g. first emitted value is ["aman","bansal"] and the second is the same ["aman","bansal"] which you don't want to emit since the values are same. So you use distinctUntilChanged to make sure it won't emit the same value until changed.
I hope this helped.

Retrofit's `enqueue()` doesn't re-assign value of object in time for the return statement?

I'm making a call to the API and the response body is assigned to an object inside Retrofit's enqueue(), the problem is that enqueue finishes too quickly for the value to be assigned before the return statement of the function body is called.
Previously, I was using MutableLiveData before and it took care of that because it's always observing the data and when it changes it assigns it with no problem but now I don't want to use any MutableLiveData or Observables because I'm trying to prepare the data before any UI is actually drawn on the screen.
fun getResponse(
weatherLocationCoordinates: WeatherLocation
): RequestResponse {
weatherApiService.getCurrentWeather(
weatherLocationCoordinates.latitude,
weatherLocationCoordinates.longitude
).enqueue(object : Callback<WeatherResponse> {
override fun onResponse(
call: Call<WeatherResponse>,
response: Response<WeatherResponse>
) {
if (response.isSuccessful) {
// This where I do the assigning
requestResponse = RequestResponse(response.body(), true)
}
}
override fun onFailure(call: Call<WeatherResponse>, t: Throwable) {
requestResponse = RequestResponse(null, false)
}
})
// When this is called, enqueue is still not finished
// therefore I get the wrong value, I get the previously set initialization value of the obj.
return requestResponse
}
Should I be using Callbacks or something else? I'm not sure on how to implement the callback.
Following up on the comments here's an approach with callbacks:
Let's suppose we change the method signature to:
fun getResponse(
weatherLocationCoordinates: WeatherLocation,
onSuccess: (WeatherResponse) -> Unit = {},
onError: (Throwable) -> Unit = {}
) {
weatherApiService.getCurrentWeather(
weatherLocationCoordinates.latitude,
weatherLocationCoordinates.longitude
).enqueue(object : Callback<WeatherResponse> {
override fun onResponse(
call: Call<WeatherResponse>,
response: Response<WeatherResponse>
) {
if (response.isSuccessful) {
onSuccess(response.body())
} else {
onError(CustomHttpExceptionWithErrorDescription(response))
}
}
override fun onFailure(call: Call<WeatherResponse>, t: Throwable) {
onError(t)
}
})
}
CustomHttpExceptionWithErrorDescription will have to be something you code that can simply parse the error gotten from the server. Anything that is not 2XX status code
This method accepts 2 extra parameters - one gets called upon success the other on error. The idea is to call it like:
getResponse(
weatherLocationCoordinates,
onSuccess = {
// do something with response
},
onError = {
// do something with the error
}
)
Because they have default parameters you actually don't need to specify both callbacks. Just the one you are interested in. Examples:
// react only to successes
getResponse(
weatherLocationCoordinates,
onSuccess = {
// do something with response
}
)
// react only to errors
getResponse(weatherLocationCoordinates) {
// do something with the error
}
// just call the network calls and don't care about success or error
getResponse(weatherLocationCoordinates)

Kotlin Object OnComplete listener

I need to know when my object is finally setup (gets data from internet) so I can properly use it without any nullpointerexceptions. Is there a way I can make an object notify when it's all setup, just like onComplete lambdas as function parameters. Simple question but anything helps :)
You can create a function type variable in your class. Set the function in your activity. Call it when your object is ready.
class MyObject()
{
var onComplete : (()-> Unit)? = null
fun setup()
{
//setup your object
onComplete?.invoke()
}
}
In your activity
val myObject = MyObject()
myObject.onComplete = {
//..things to do after setup...
}
myObject.setup()
You can also put it in the constructor if you want.
In android, we can use LiveData if you added support library dependency.
class Result
object MyObject {
val onComplete: MutableLiveData<Result> = MutableLiveData()
fun setUp() {
// do something
onComplete.setValue(Result()) // main thread
// onComplete.postValue(Result()) // other thread
}
}
class SampleActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
MyObject.onComplete.observe(this, Observer { result ->
// do something with result
})
MyObject.setUp()
}
}
If this method is unfamiliar, check out observer pattern or LiveData.