How to return value from async coroutine scope such as ViewModelScope to your UI?
Asked Answered
G

3

14

I'm trying to retrieve a single entry from the Database and successfully getting the value back in my View Model with the help of viewModelScope, but I want this value to be returned back to the calling function which resides in the fragment so it can be displayed on a TextView.

I tried to return the value the conventional way but it didn't work. So, How Can I return this value from viewModelScope.launch to the calling function?

View Model

fun findbyID(id: Int) {
    viewModelScope.launch {
        val returnedrepo = repo.delete(id)
        Log.e(TAG, returnedrepo.toString())
        // how to return value from here to Fragment
    }
}

Repository

suspend fun findbyID(id: Int): userentity {
    val returneddao = Dao.findbyID(id)
    Log.e(TAG, returneddao.toString())
    return returneddao
}
Graeme answered 29/3, 2020 at 6:44 Comment(0)
G
1

Thank you Nataraj KR for your Help!

Following is the code that worked for me.

View Model

class ViewModel(application: Application):AndroidViewModel(application) {
val TAG = "ViewModel"
val repo: theRepository
val alldata:LiveData<List<userentity>>
val returnedVal = MutableLiveData<userentity>()
init {
    val getDao = UserRoomDatabase.getDatabase(application).userDao()
    repo = theRepository(getDao)
    alldata = repo.allUsers

}

fun findbyID(id: Int){
    viewModelScope.launch {
       returnedVal.value = repo.findbyID(id)
    }
}

}

Fragment

 override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)

    val usermodel = ViewModelProvider(this).get(ViewModel::class.java)
    usermodel.alldata.observe(this, Observer {
        Log.e(TAG,usermodel.alldata.value.toString())
    })
    usermodel.returnedVal.observe(this, Observer {
        tv1.text = usermodel.returnedVal.value.toString()
    })

    allData.setOnClickListener {
        tv1.text = usermodel.alldata.value.toString()
    }

    findByID.setOnClickListener {
        usermodel.findbyID(et2.text.toString().toInt())
    }
}
Graeme answered 30/3, 2020 at 6:6 Comment(0)
O
21

LiveData can be used to get value from ViewModel to Fragment.

Make the function findbyID return LiveData and observe it in the fragment.

Function in ViewModel

fun findbyID(id: Int): LiveData</*your data type*/> {
    val result = MutableLiveData</*your data type*/>()
    viewModelScope.launch {
       val returnedrepo = repo.delete(id)
       result.postValue(returnedrepo)
    }
    return result.
}

Observer in Fragment

findbyId.observer(viewLifeCycleOwner, Observer { returnedrepo ->
   /* logic to set the textview */
})
Ovule answered 29/3, 2020 at 7:1 Comment(5)
I've sorted the issue with the Nullpointerexception and I'm finally receiving the value back in the Fragment but I'm receiving same value three times, instead of one. Any thought how I can fix that?Graeme
Check if the value is being posted three times to the livedata...Ovule
Thanks Nataraj, your idea of using MutableLiveData for transferring the value to the UI was superb.I had to sort a few other things out, but I have finally manage to get one value back to the UI. I am going to post the answer in a minute. Thank you again!Graeme
For this code to work, I had to make my fragment implement LifecycleObserver.Eductive
Sorry, worked once but know, even with LifecycleObserver implemented on fragment, it is not working anymore.Eductive
S
2

Another way without using LiveData would be like this,

Similar to viewModelScope there is also a lifecycleScope available with lifecycle-aware components, which can be used from the UI layer. Following is the example,

Fragment

override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)
    findByID.setOnClickListener {
        lifecycleScope.launch{
            val res = usermodel.findbyID(et2.text.toString().toInt())
            // use returned value to do anything.
        }
    }
}

ViewModel

//1st option
// make the function suspendable itself.use aync instead of launch and then
// use await to collect the returned value.
suspend fun findbyID(id: Int): userEntity  {
    val job = viewModelScope.async {
       val returnedrepo = repo.delete(id)
       Log.e(TAG,returnedrepo.toString())
       return@async returnedrepo
    }
    return job.await()
}

//2nd option
// make the function suspendable itself. but switch the execution on IO
// thread.(since you are making a DB call)
suspend fun findbyID(id: Int): userEntity  {
    return withContext(Dispatchers.IO){
       val returnedrepo = repo.delete(id)
       Log.e(TAG,returnedrepo.toString())
       return@withContext returnedrepo
    }
}

Since LiveData is specific to Android Environment, Using Kotlin Flow becomes a better option in some places, which offers similar functionality.

Savitt answered 25/8, 2022 at 4:10 Comment(2)
Hm, disagree, using suspending functions in the ViewModels is not advisable since now the ViewModel might be accessed inside a totally different CoroutineScope which doesn't match the scope of the ViewModel. If you install the SonarLint code checker it also warns about this as a CodeSmell with severity "Major"Jagged
Well, that will depend on the case. Some cases may require the caller scope to be less than the ViewModel scope because the operation/data is only required as long as the caller is active. In some cases, where operation/data is required irrespective of the caller lifecycle, suspend function will not make sense, and ViewModel or any external scope(injected from outside) with a higher appropriate lifecycle should be used. Moreover for single-shot requests suspend functions seem more suitable than LiveData or even Kotlin Flows which are better suited for a stream of data.Savitt
G
1

Thank you Nataraj KR for your Help!

Following is the code that worked for me.

View Model

class ViewModel(application: Application):AndroidViewModel(application) {
val TAG = "ViewModel"
val repo: theRepository
val alldata:LiveData<List<userentity>>
val returnedVal = MutableLiveData<userentity>()
init {
    val getDao = UserRoomDatabase.getDatabase(application).userDao()
    repo = theRepository(getDao)
    alldata = repo.allUsers

}

fun findbyID(id: Int){
    viewModelScope.launch {
       returnedVal.value = repo.findbyID(id)
    }
}

}

Fragment

 override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)

    val usermodel = ViewModelProvider(this).get(ViewModel::class.java)
    usermodel.alldata.observe(this, Observer {
        Log.e(TAG,usermodel.alldata.value.toString())
    })
    usermodel.returnedVal.observe(this, Observer {
        tv1.text = usermodel.returnedVal.value.toString()
    })

    allData.setOnClickListener {
        tv1.text = usermodel.alldata.value.toString()
    }

    findByID.setOnClickListener {
        usermodel.findbyID(et2.text.toString().toInt())
    }
}
Graeme answered 30/3, 2020 at 6:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.