Mutate MutableLiveData within viewModelScope or not? - android

repository.callPermissionRemove(permissionID) is a suspending function which requires a viewModelScope to call within a viewModel. After calling the suspending function in the repository, I would like to reset the MutableLiveData back to null.
May I know what's the difference between these two sets of code and which one should I implement?
Code A
fun callPermissionRemove(permissionID: Int) {
viewModelScope.launch {
permissionRemoveAPIResponse.value = repository.callPermissionRemove(permissionID)
}
permissionRemoveAPIResponse.value = null
}
Code B
fun callPermissionRemove(permissionID: Int) {
viewModelScope.launch {
permissionRemoveAPIResponse.value = repository.callPermissionRemove(permissionID)
permissionRemoveAPIResponse.value = null
}
}

In your Code A example, line, setting response value to null will get called before repository.callPermissionRemove() will execute, whereas in the Code B example, lines will execute in the same order as written.
That being said, when resetting MutableLiveData field to null after use, you might want to consider using SingleLiveEvent. Example of it you can find here, and some more explanation of why and how here.

Related

ViewModel + Room test coverage in UnitTest

I have an unit test like this:
...
subj.mintToken(to, value, uri)
advanceUntilIdle()
...
val pendingTxFinalState = subj.uiState.value.pendingTx.count()
assertThat("Model should have a single pending tx, but has $pendingTxFinalState", pendingTxFinalState == 1)
...
The model field in ViewModel is populated by the request to cache in the init {} block. Each change in table would trigger this coroutine flow. This piece of unit test checks correctness of this functionality.
The current issue is this Flow in init {} block is triggered only on the test start when ViewModel instance is created. It does not respond on update in table.
It is important to note I don't use in test a room database neither test database, but FakeCacheRepository where behaviour of methods are emulated by flow with mocked data. However the behaviour of flow should be the same as there is still in change in underlying data.
val txPool = ConcurrentLinkedQueue<ITransaction>()
override fun createChainTx(tx: ITransaction): Flow<ITransaction> {
return flow {
txPool.add(tx)
emit(tx)
}
}
override fun getAllChainTransactions(): Flow<List<ITransaction>> {
return flow {
emit(txPool.toList())
}
}
Do you see the issue here or better way to test this?
My guess is you’re writing you’re own FakeCacheRepo and in the update function you are calling createChainTx. The value of the flow isn’t updating though because the create function doesn’t just update the value it creates a new flow instead of updating the old one. You can modify the set up to emit continuously in a loop (with some buffer delay) based on a variable. Then when you change the variable it will change what the current flow is emiting as expected.
The code example here is roughly doing that: https://developer.android.com/kotlin/flow#create
override fun createChainTx(): Flow<ITransaction> {
return flow {
while(true) {
val tx = getLatestTxValue() // Get the latest updated value from an outside source
txPool.add(tx)
emit(tx)
delay(refreshIntervalMs) // Suspends the coroutine for some time
}
}
}

How can I get data from ViewModel to Activity or Fragment in clean and simple way?

I have a question... sometimes, I need to get data from ViewModel directly. For example, Let's say there's a isChecked() method in ViewModel. And I want to use it in the if condition.
if(viewModel.isChecked()){
// TODO:
}
So, what I am doing right now is:
fun isChecked(): Boolean = runBlocking {
val result = dbRepo.getData()
val response = apiRepo.check(result)
return response.isSuccessful
}
It uses runBlocking. So, it runs on MainThread. I don't think it's a good way because it can freeze the screen. But yes, if the condition needs to run, it needs to wait it until it gets the data from DB and Network.
Another way that I can think of is using LiveData. However, I can't use it in the condition. So, I needs to move the condition in the observer block. But sometimes, this can't be done because there can be something before the condition. And it doesn't seem to look direct but writing code here and there and finally get that data.
So, Is there any simpler way than this?
Your best bet if you have something slow or blocking like that is to rethink how you are using the data entirely. Instead of trying to return it, use LiveData or callbacks to handle the response asynchronously without causing your UI to hang or become laggy. In these cases you really only have three options:
Use a callback to handle when the response is received
Use observable data like LiveData to handle when the response is received
Change the method to a suspend function and call it from a coroutine
Forcing a method to wait to return on the main thread without using one of these is going to cause the app to hang.
Callback to get state
It's hard to say definitely what the best solution for you is without more details about how you are using isChecked(), but one pattern that could work would be to use a callback to handle what you were formerly putting in the if statement, like this (in the ViewModel):
fun getCheckedState(callback: (Boolean)->Unit) {
viewModelScope.launch {
// do long-running task to get checked state,
// using an appropriate dispatcher if needed
val result = dbRepo.getData()
val response = apiRepo.check(result)
// pass "response.isSuccessful" to the callback, to be
// used as "isChecked" below
callback(response.isSuccessful)
}
}
You would call that from the activity or fragment like this:
viewModel.getCheckedState { isChecked ->
if( isChecked ) {
// do something
}
else {
// do something else
}
}
// CAUTION: Do NOT try to use variables you set inside
// the callback out here!
A word of caution - the code inside the callback you pass to getCheckedState does not run right away. Do not try to use things you set inside there outside the callback scope or you fall into this common issue
Simpler Callback
Alternately, if you only want to run some code when isChecked is true, you could simplify the callback like this
fun runIfChecked(callback: ()->Unit) {
viewModelScope.launch {
// do long-running task to get checked state,
// using an appropriate dispatcher if needed
val result = dbRepo.getData()
val response = apiRepo.check(result)
// only call the callback when it's true
if( response.isSuccessful ) {
callback()
}
}
}
and call it with
viewModel.runIfChecked {
// do something
}
// Again, don't try to use things from the callback out here!
Use lifecyclescope.launch(Dispatcher.IO) instead of runblocking
Try this code on your ViewModel class:
suspend fun isChecked(): Boolean {
val response: Response? = null
viewModelScope.launch(Dispatchers.IO) {
val result = dbRepo.getData()
response = apiRepo.check(result)
}.join()
return response?.isSuccessful
}
From Activity:
// Suppose you have a button
findViewById<Button>(R.id.btn).setOnClickListener({
CoroutineScope(Dispatchers.Main).launch {
if (viewModel.isChecked()) {
Log.d("CT", "Do your others staff")
}
}
})
Hope it work file. If no let me comment

Thread safe LiveData updates

I have the following code which has a race condition. I try to find an item in a list and set its loading property. But if onLoaded("A") and onLoaded("B") are called multiple times from different threads. I always lose the data of the first call if it doesn't complete before second starts.
How can I make this work? Is using Mutex should be the correct approach?
val list = MutableLiveData<List<Model>>() // assume this is initialized with ["Model(false, "A"), Model(false, "B")]
data class Model(
val loaded: Boolean,
val item: String,
)
fun onLoaded(item: String) = viewModelScope.launch {
val currList = list.value ?: return#launch
withContext(Dispatchers.Default) {
val updated = currList.find { it.item == item }?.copy(loaded = true)
val mutable = currList.toMutableList()
updated?.let {
val index = mutable.indexOf(it)
mutable[index] = it
}
list.postValue(mutable.toList())
}
}
onLoaded("A")
onLoaded("B")
expected: ["Model(true, "A"), Model(true, "B")]
actual: ["Model(false, "A"), Model(true, "B")]
In onLoaded() a new coroutine is launched using viewModelScope. viewModelScope has Dispatchers.Main.immediate context, so the code inside it will be executed on the Main Thread, e.g. execution is limited to only one thread. The reason you have a Race Condition because calling the onLoaded() function consecutively doesn't guarantee the order of coroutines execution.
If you call onLoaded() consecutively from one thread I suggest to remove launching a coroutine viewModelScope.launch in it. Then the order of calling will be preserved. Use list.postValue() in this case.
If you call onLoaded() from different threads and still want to launch a coroutine you can refer to answers to this question.
Try to use #Synchronized anotation without launching a coroutine:
#Synchronized
fun onLoaded(item: String) { ... }
Method will be protected from concurrent execution by multiple threads by the monitor of the instance on which the method is defined. Use list.postValue() in this case.

Getting wrong result from the inner functions call with the use of Kotlin co-routines

I have created below function :
suspend fun isBatteryExistsInLocal(batteryId: String): Boolean {
val count = appDatabase.userBatteriesDao().checkBatteryExists(batteryId)
if (count > 0) {
return true
} else {
return false
}
}
which checks the particular record is exists in database or not.
checkBatteryExists is the dao method as below :
#Query("SELECT COUNT(*) FROM " + DatabaseConstant.mUserBatteriesTable + " WHERE isDeleted = 0 and batteryId= :batteryId")
suspend fun checkBatteryExists(batteryId:String): Int
The Method isBatteryExistsInLocal is called from below function in my view model class.
fun isBatteryExistsInLocal(batteryId:String): Boolean {
var isBatteryExistsInLocal = false
scope.launch {
isBatteryExistsInLocal =batteryRepository.isBatteryExistsInLocal(batteryId)
}
return isBatteryExistsInLocal
}
and the above method is calling from my fragment as below :
if (viewModel.isBatteryExistsInLocal(listNotifications[adapterPosition].batteryId)) {
But here, I am always getting false WHY ?
I have debugged and check that a record is exists in local database and also count variable is returns 1.
Then Why am getting false in above if condition ?
Please guide. What I am doing wrong.
Thanks.
Contrary to what you may have expected the function in ViewModel executes in following manner
fun isBatteryExistsInLocal(batteryId:String): Boolean {
var isBatteryExistsInLocal = false
scope.launch {
// Everything inside will be executed async
// outer function may already have returned by the time this completes
}
return isBatteryExistsInLocal
}
One way to fix this is to put the fragment code (viewModel.isBatteryExistsInLocal(listNotifications[adapterPosition].batteryId)) inside a coroutine, which can be done as
lifecycleScope.launch{
(viewModel.isBatteryExistsInLocal(listNotifications[adapterPosition].batteryId))
// other code
}
And change your ViewModel function to be suspending as
suspend fun isBatteryExistsInLocal(batteryId:String): Boolean {
return batteryRepository.isBatteryExistsInLocal(batteryId)
}
When you launch a coroutine, it is launched asynchronously, just like when you call a function that takes a callback. The coroutine is queued to start running, but the current function will possibly complete and return before that happens. Explanations of this issue are in the answers of this question even though they are about API callbacks. It's exactly the same issue when launching a coroutine.
The only way to convert a suspend function into one you can call from outside a coroutine to get a return value synchronously is to do something like use runBlocking or calling join on your launched Job. But this is not an acceptable solution because it will block your main thread and cause stutters or the ANR error.
The correct solution is to use a coroutine higher up in your workflow so you can freely use suspend functions wherever you need to. For instance, if isBatteryExistsInLocal is something you need as part of a response to some button press, you should launch a coroutine in your button's click listener, and then your entire sequence of actions can freely include suspend function calls.
By the way, just a tip, you can simplfy your suspend function. Instead of using if/else to return true or false, you could simply put return count > 0.

Why can't I call kotlin suspend function under lambda function

Let me start with example code snippets
suspend fun executeLive(result: MutableLiveData<Person>) {
val response = ... //suspend api request
mediatorLiveData.removeSource(response)
mediatorLiveData.addSource(response) {
result.value = sortData(it) // sortData is also suspend function which sortData at Dispatcher.Default
}
}
In this example, sortData can't call under lambda function(in this case addSource).And also I already declare executeLive as suspend, that why suspend api request can start at first. But sortData function show compile time error
Suspend function can only be called from a coroutine body
So how do I change my code structure to solve this problems?
Update: Is there any article about this?
A lambda is generally a callback function. Callback functions are so called because we wrap a block of code in a function, and pass it to someone else (or some place else) to be executed. It is a basic inversion of control where the code is not for you to execute, but someone else to do it (example the framework).
For example when you set a onClickListener on a button, we don't know when it will get called, we pass a lambda for the framework which takes care of the user interaction to call the specified action.
In your case similarly the suspend function is not calling the sortdata, it is passing it to the mediatorLiveData object to call it in its own context. It is not necessary the lambda you passed would be called from a coroutine body, as such this is not allowed.
You can solve this by converting the mediatorLiveData.addSource call into a suspending call itself with suspendCoroutine:
suspend fun executeLive(result: MutableLiveData<Person>) {
val response = ... //suspend api request
mediatorLiveData.removeSource(response)
val data = suspendCoroutine<TypeOfData> { cont ->
mediatorLiveData.addSource(response) { cont.resume(it) }
}
result.value = sortData(data)
}
I've used TypeOfData as a placeholder for whatever the type of data emitted by response is. Note that this will only work if the you're intending for a single emission to happen, though.
If you need to track multiple values, you can experiment with callbackFlow:
suspend fun executeLive(result: MutableLiveData<Person>) {
val response = ... //suspend api request
mediatorLiveData.removeSource(response)
callbackFlow<TypeOfData> {
mediatorLiveData.addSource(response) { offer(it) }
awaitClose { mediatorLiveData.removeSource(response) }
}
.collect { result.value = sortData(it) }
}

Categories

Resources