I am new to mvvm. So I want to ask how to retrieve the result of the method executed in viewModelScope.
In my example I want to save a book in database and retrieve the saved book. Is there a better way to do it?
fun addBook (book:Book): BookEntity {
var bookEntity = BookEntity()
viewModelScope.launch {
bookEntity = repository.addBook(book)
}
return bookEntity
}
The coroutine, launched in addBook method using launch builder, will be executed after the function returns, so bookEntity will no be reassigned with a new value from DB. You should think about how you want to use the data. If you want it to be used just as an input data for some another calculations then it make sense to make the addBook() function suspend:
suspend fun addBook(): BookEntity {
val bookEntity = repository.addBook(book) // I assume function repository.addBook(book) is suspend
return bookEntity
}
If you want it to be displayed in UI you can make it suspend like above and call it in a coroutine using lifecycleScope:
in Activity/Fragment
lifecycleScope.launch {
val book = viewModel.addBook()
// update UI
}
Another alternative is to apply reactive approach using LiveData or Kotlin Flow
Related
I use livedata that collect item data, after it in onClicked() I get this data from livedata. What could be better approach for this?
lifecycleScope.launch {
lifecycleScope.async {
viewModel.fetchItem(args.workId)
}.await()
onClicked()
}
variables in viewModel
val item = _item.immutable()
private val _item = MutableLiveData<ViewState<Item?>>()
[...]
// I wait here for data from first code than I collect it because item is liveData as above
private fun onClicked() {
val item = viewModel.item.value?.dataOrNull
[...]
fetchItem method
fun fetchItem(id: Int) {
viewModelScope.launch {
_item.postAsyncValue { repository.getItem(id) }
}
}
Currently, I think your code is not doing what you think it does. fetchItem is not a suspend function. It launches a coroutine and immediately returns without waiting for the coroutine to finish. So, your async coroutine that calls it doesn't wait for that result either. There's no point in using async at all here since the code returns almost immediately.
I'm guessing that what you're trying to accomplish is wait for postAsyncValue to finish setting that new value. To do this, you need to make fetchItem a suspend function that waits for its work to be done.
I am not familiar with this postAsyncValue, but my best guess is that it is a helper extension function on MutableLiveData that takes a suspend lambda, calls it, and then sets the value to the LiveData on the main thread. If this is the case, you should just do this instead so the function actually waits for the task to be done before returning:
suspend fun fetchItem(id: Int) = withContext(Dispatchers.Main) {
_item.value = repository.getItem(id) // assuming getItem is a suspend function
}
//...
lifecycleScope.launch {
viewModel.fetchItem(args.workId)
onClicked()
}
If this isn't right, please add your source code for postAsyncValue and let me know.
Regarding your literal question, instead of using async followed immediately by await, you can use withContext(Dispatchers.IO) { }, but you would only do this if you are calling blocking code. You only need async when you're working with parallelism, so the compiler warns you that you're doing something silly if you immediately call await() after async { }.
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.
I've to update an item in a fragment based on the data returned from the network call. I don't want to block the main thread and end up with ANR so I adopted the approach of "callbacks" however I wonder if there's a way to wait for the result from the network call without relying on the callback mechanism using coroutines
Current implementation
MyFragment.kt
fun updateButtonText() {
handlerClass.getData {
//received data from the server update the button text
}
}
HandlerClass.kt
fun getData(callback: (String) -> Unit) {
scope.launch(Dispatchers.IO) {
val data = mySuspendedNetworkcallMethod()
callback.invoke(data)
}
}
Desired Implementation:
MyFragment.kt
fun updateButtonText() {
val data = handlerClass.getData()
button.text = data
}
HandlerClass.kt
suspend fun getData() {
return mySuspendedNetworkcallMethod()
}
For the desired demo implementation, I understand, I'd have to use runBlocking{} to call a suspended method however runBlocking{} will block the calling thread - which in this case would be the Main Thread until getData() returns the data.
I don't want to block the main thread but still be able to cal & wait for the suspended method to fetch the data and then update the button.
Coroutines are designed to get rid of callbacks. You can use lifecycleScope in the Fragment class to launch a lifecycle-aware coroutine, it will look like the following:
MyFragment.kt:
fun updateButtonText() = lifecycleScope.launch {
button.text = handlerClass.getData()
}
HandlerClass.kt:
suspend fun getData() {
return mySuspendedNetworkcallMethod()
}
If you use MVVM approach you should consider to use ViewModel and it's viewModelScope extension to launch coroutines.
For LifecycleScope, use androidx.lifecycle:lifecycle-runtime-ktx:2.4.0 or higher.
For ViewModelScope, use androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0 or higher.
Well recommended way is to use viewmodel and viewmodelscope for suspend functions.
However in your situation, just use lifecyclescope
fun updateButtonText() {
lifecycleScope.launch{
val data = handlerClass.getData()
button.text = data
}
}
https://developer.android.com/topic/libraries/architecture/coroutines
I hope to get the total of all records with Room database at once. But, normally Room use background thread to query record asynchronously.
If I use getTotalOfVoiceAsLiveData() in Code A, it will return LiveData<Long>, you know that LiveData variable is lazy, maybe the result is null.
If I use getTotalOfVoice() in Code A, I will get error because I can't use return in viewModelScope.launch{ }.
How can I get the total of all records at once with Room database?
Code A
class HomeViewModel(val mApplication: Application, private val mDBVoiceRepository: DBVoiceRepository) : AndroidViewModel(mApplication) {
fun getTotalOfVoice():Long {
viewModelScope.launch {
return mDBVoiceRepository.getTotalOfVoice() //It will cause error
}
}
fun getTotalOfVoiceAsLiveData(): LiveData<Long>{
return mDBVoiceRepository.getTotalOfVoiceAsLiveData() //It's lazy, maybe the result is null.
}
}
class DBVoiceRepository private constructor(private val mDBVoiceDao: DBVoiceDao){
suspend fun getTotalOfVoice() = mDBVoiceDao.getTotalOfVoice()
fun getTotalOfVoiceAsLiveData() = mDBVoiceDao.getTotalOfVoiceAsLiveData()
}
#Dao
interface DBVoiceDao{
#Query("SELECT count(id) FROM voice_table")
suspend fun getTotalOfVoice(): Long
//When Room queries return LiveData, the queries are automatically run asynchronously on a background thread.
#Query("SELECT count(id) FROM voice_table")
fun getTotalOfVoiceAsLiveData(): LiveData<Long>
}
Add content
To Tobi: Thanks!
Why it is important to you to get the data directly?
I need to generate a filename based the total of the records, such as "untitled0", "untitled1", "untitled2"...
If I can get the query result at once, I can use the following code easyly.
Added again
I hope to record a voice by filename based the total of query records when I click Start button. You know the total of records will change when a reocrd is added or deleted!
Code B
fun getTotalOfVoice():Long {
//Get the query result at once
...
}
fun createdFileanme(){
return "untitled"+getTotalOfVoice().toString()
}
btnStart.setOnClickListener{
recordVoice(createdFileanme()) //I will record voice by filename
}
fun addRecord(){
...
}
fun deleteRecord(){
...
}
New added content
Thanks!
I think 'You should also move all of that into the viewmodel class, without LiveData ' is good way, you can see Image A and How can I get the value of a LivaData<String> at once in Android Studio? .
Do you agree with it?
Image A
Question: at once meaning synchronous or what ? if yes, what happens if the function to get the result has to take a longer time? like network call? well you can decide to do that on another thread.
What I think is for you to use a mutable Object and use the postValue function to dispatch the result to the observers. It should look something like below:
class HomeViewModel(val mApplication: Application, private val mDBVoiceRepository: DBVoiceRepository) : AndroidViewModel(mApplication) {
private val voices = MutableLiveData<Long>()
fun getTotalOfVoiceAsLiveData(): LiveData<Long> {
voices.postValue(mDBVoiceRepository.getTotalOfVoiceAsLiveData().value)
return voices;
}
}
Making use of it in your Fragment will look like below:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
if (activity != null) {
val viewModel = ViewModelProvider(requireActivity())
viewModel.get(HomeViewModel::class.java).getTotalOfVoiceAsLiveData().observe(viewLifecycleOwner, Observer { voices: Long ? ->
voices // Sound of music ? be very free to use ...
})
}
}
Happy Coding.
I hope to get the result at once, but LiveData is lazy
Sorry to tell, but this is how the Room interface is designed.
You are right with the lazyness of the returned LiveData object. But this allows you to handle it on a different thread without having to manually handle different threads.
Based on your new information!
You basically have two options:
A) you could do the following:
load data from Room via LivaData
add observer that stores the current total amount
when the button is clicked you just read the local copy
In your View: (only one observer and one clickListener)
val totalVoiceCount: long
val viewModel = ViewModelProvider(requireActivity()).get(HomeViewModel::class.java)
viewModel.getTotalOfVoiceAsLiveData().observe(viewLifecycleOwner, Observer { totalOfVoice : Long ? ->
if (totalOfVoice != null)
totalVoiceCount = totalOfVoice
})
btnStart.setOnClickListener{
viewModel.recordVoice(totalVoiceCount)
}
In your ViewModel: (the logic and everything else)
fun recordVoice(totalVoiceCount : long){
val fileName = createdFileanme(totalVoiceCount)
// create your recording // depending on how you do this, it probably runs on a background thread anyways
}
fun createdFileName(totalVoiceCount : long){
return "untitled"+ String.valueOf(totalVoiceCount)
}
This works reliably because the LiveData has enough time to update the local copy of totalVoiceCount before the user has the chance to click the button.
B) Based on the answer in your parallel question you can of course outsource even more to a background thread. Then you also have the option to call the DAO query with a non-livedata return (as room returns non-livedata queries only on background threads). Is it worth to implement the threading suggestion of Ridcully? Not possible to answer without knowing what else is going on simultaneously... To me it seems like an overkill, but he is right that the more you do on background threads the better for your refresh rate..
You can return Deferred<Long> from viewModelScope.async. I recommend you to use:
val deferred = viewModelScope.async(Dispatchers.IO) {
return#async mDBVoiceRepository.getTotalOfVoice()
}
val value = deferred.await()
await() is suspend
Edit:
If you want to get a getter which will use in your activity or fragment
you need to write a suspend function like this:
suspend fun getTotalOfVoice(): Long {
return viewModelScope.async(Dispatchers.IO) {
return#async mDBVoiceRepository.getTotalOfVoice()
}.await()
}
But mvvm pattern allows you to create LiveData inside your ViewModel, which gives your fragment an observer.
In view model:
private val _totalOfVoiceLD: MutableLiveData<Long> = MutableLiveData()
val totalOfVoiceLD: LiveData<Long>
get() = _totalOfVoiceLD
fun updateTotalOfVoice() {
viewModelScope.launch(Dispatchers.IO) {
val totalOfVoice = mDBVoiceRepository.getTotalOfVoice()
_totalOfVoiceLD.postValue(totalOfVoice)
}
}
and in your fragment:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.totalOfVoiceLD.observe(viewLifecycleOwner, Observer { totalOfVoice ->
totalOfVoiceTextView.text = totalOfVoice.toString()
})
}
You can use coroutineContext.async to get data from DB and wait for getting it's response with data by using .await function for a async dispatch.
suspend fun getAllVoices() : Long{
val awatingResults = viewModelScope.async(Dispatchers.IO) {
mDBVoiceRepository.getTotalOfVoice()
}
val records = awatingResults.await()
return records
}
It is necessary to call a Suspend function from a coroutine and
async.await() is always called in a suspended function so,
val voiceLiveData: MutableLiveData<Long> = MutableLiveData()
fun getAllVoicesFromDB() {
viewModelScope.launch(Dispatchers.IO) {
voiceLiveData.postValue(mDBVoiceRepository.getTotalOfVoice())
}
}
Now call it where ever you want to get your voice data from database and also remember do your further work inside your voiceLiveData observer where you get your response of voices :)
Live data is designed to be lazy, when the value of the live data changes internally it emits and wherever you are observing it, the onChange function will be invoked. It is designed to fire and forget.
Because room uses background thread to run the query.
You can't expect live data to behave like sharedpreference where you store key value pair.
If you want to achieve something like that.
I would suggest you to use
Paper Db or Realm.
If you need your Room result synchronously, your code should be execute in IO thread. In case of coroutines, you can use Dispatchers.IO. Your code can be changed to this to pass the error.
class HomeViewModel(val mApplication: Application, private val mDBVoiceRepository: DBVoiceRepository) : AndroidViewModel(mApplication) {
fun getTotalOfVoice():Long {
viewModelScope.launch(Dispatchers.IO) { // here
return mDBVoiceRepository.getTotalOfVoice()
}
}
}
If you must run the queries in the main thread, then:
Allow android room to execute queries in main thread.
val dbInstance = Room
.databaseBuilder(ctx, YourDBClass::class.java, "YourDBName")
.allowMainThreadQueries()
.build()
Define the dao method as follows
#Dao
interface DBVoiceDao{
#Query("SELECT count(id) FROM voice_table")
fun getTotalOfVoice(): Long
}
Access the method in the repository
fun getTotalOfVoice():Long {
return dao.getTotalOfVoice()
}
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) }
}