How to call again LiveData Coroutine Block - android

I'm using LiveData's version "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-alpha05". Once my LiveData block executes successfully I want to explicitly trigger it to execute again, e.g.
I navigate to a fragment
User's data loads
I click delete btn while being in the same fragment
User's data should refresh
I have a fragment where I observe my LiveData, a ViewModel with LiveData and Repository:
ViewModel:
fun getUserLiveData() = liveData(Dispatchers.IO) {
val userData = usersRepo.getUser(userId)
emit(userData)
}
Fragment:
viewModel.getUserLiveData.observe(viewLifecycleOwner,
androidx.lifecycle.Observer {..
Then I'm trying to achieve desired behaviour like this:
viewModel.deleteUser()
viewModel.getUserLiveData()
According to the documentation below LiveData block won't execute if it has completed successfully and if I put a while(true) inside the LiveData block, then my data refreshes, however I don't want this to do since I need to update my view reactively.
If the [block] completes successfully or is cancelled due to reasons other than [LiveData]
becoming inactive, it will not be re-executed even after [LiveData] goes through active
inactive cycle.
Perhaps I'm missing something how I can reuse the same LiveDataScope to achieve this? Any help would be appreciated.

To do this with liveData { .. } block you need to define some source of commands and then subscribe to them in a block. Example:
MyViewModel() : ViewModel() {
val commandsChannel = Channel<Command>()
val liveData = livedata {
commandsChannel.consumeEach { command ->
// you could have different kind of commands
//or emit just Unit to notify, that refresh is needed
val newData = getSomeNewData()
emit(newData)
}
}
fun deleteUser() {
.... // delete user
commandsChannel.send(RefreshUsersListCommand)
}
}
Question you should ask yourself: Maybe it would be easier to use ordinary MutableLiveData instead, and mutate its value by yourself?
livedata { ... } builder works well, when you can collect some stream of data (like a Flow / Flowable from Room DB) and not so well for plain, non stream sources, which you need to ask for data by yourself.

I found a solution for this. We can use switchMap to call the LiveDataScope manually.
First, let see the official example for switchMap:
/**
* Here is an example class that holds a typed-in name of a user
* `String` (such as from an `EditText`) in a [MutableLiveData] and
* returns a `LiveData` containing a List of `User` objects for users that have
* that name. It populates that `LiveData` by requerying a repository-pattern object
* each time the typed name changes.
* <p>
* This `ViewModel` would permit the observing UI to update "live" as the user ID text
* changes.
**/
class UserViewModel: AndroidViewModel {
val nameQueryLiveData : MutableLiveData<String> = ...
fun usersWithNameLiveData(): LiveData<List<String>> = nameQueryLiveData.switchMap {
name -> myDataSource.usersWithNameLiveData(name)
}
fun setNameQuery(val name: String) {
this.nameQueryLiveData.value = name;
}
}
The example was very clear. We just need to change nameQueryLiveData to your own type and then combine it with LiveDataScope. Such as:
class UserViewModel: AndroidViewModel {
val _action : MutableLiveData<NetworkAction> = ...
fun usersWithNameLiveData(): LiveData<List<String>> = _action.switchMap {
action -> liveData(Dispatchers.IO){
when (action) {
Init -> {
// first network request or fragment reusing
// check cache or something you saved.
val cache = getCache()
if (cache == null) {
// real fecth data from network
cache = repo.loadData()
}
saveCache(cache)
emit(cache)
}
Reload -> {
val ret = repo.loadData()
saveCache(ret)
emit(ret)
}
}
}
}
// call this in activity, fragment or any view
fun fetchData(ac: NetworkAction) {
this._action.value = ac;
}
sealed class NetworkAction{
object Init:NetworkAction()
object Reload:NetworkAction()
}
}

First add implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" to your gradle file. Make your ViewModel as follows:
MyViewModel() : ViewModel() {
val userList = MutableLiveData<MutableList<User>>()
fun getUserList() {
viewModelScope.launch {
userList.postValue(usersRepo.getUser(userId))
}
}
}
Then onserve the userList:
viewModel.sessionChartData.observe(viewLifecycleOwner, Observer { users ->
// Do whatever you want with "users" data
})
Make an extension to delete single user from userList and get notified:
fun <T> MutableLiveData<MutableList<T>>.removeItemAt(index: Int) {
if (!this.value.isNullOrEmpty()) {
val oldValue = this.value
oldValue?.removeAt(index)
this.value = oldValue
} else {
this.value = mutableListOf()
}
}
Call that extension function to delete any user and you will be notified in your Observer block after one user get deleted.
viewModel.userList.removeItemAt(5) // Index 5
When you want to get userList from data source just call viewModel.getUserList() You will get data to the observer block.

private val usersLiveData = liveData(Dispatchers.IO) {
val retrievedUsers = MyApplication.moodle.getEnrolledUsersCoroutine(course)
repo.users = retrievedUsers
roles.postValue(repo.findRolesByAll())
emit(retrievedUsers)
}
init {
usersMediator.addSource(usersLiveData){ usersMediator.value = it }
}
fun refreshUsers() {
usersMediator.removeSource(usersLiveData)
usersMediator.addSource(usersLiveData) { usersMediator.value = it }
The commands in liveData block {} doesn't get executed again.
Okay yes, the observer in the viewmodel holding activity get's triggered, but with old data.
No further network call.
Sad. Very sad. "Solution" seemed promisingly and less boilerplaty compared to the other suggestions with Channel and SwitchMap mechanisms.

You can use MediatorLiveData for this.
The following is a gist of how you may be able to achieve this.
class YourViewModel : ViewModel() {
val mediatorLiveData = MediatorLiveData<String>()
private val liveData = liveData<String> { }
init {
mediatorLiveData.addSource(liveData){mediatorLiveData.value = it}
}
fun refresh() {
mediatorLiveData.removeSource(liveData)
mediatorLiveData.addSource(liveData) {mediatorLiveData.value = it}
}
}
Expose mediatorLiveData to your View and observe() the same, call refresh() when your user is deleted and the rest should work as is.

Related

How can I get data and initialize a field in viewmodel using kotlin coroutines and without a latenite of null field

I have a common situation of getting data. I use the Kotlin Coroutines.
1 variant:
class SomeViewModel(
private val gettingData: GetDataUseCase
) : ViewModel() {
lateinit var data: List<String>
init {
viewModelScope.launch {
data = gettingData.get()
}
}
}
2 variant:
class SomeViewModel(
private val gettingData: GetDataUseCase
) : ViewModel() {
val data = MutableStateFlow<List<String>?>(null)
init {
viewModelScope.launch {
data.emit(gettingData.get())
}
}
}
How can I initialize a data field not delayed, but immediately, with the viewModelScope but without a lateinit or nullble field? And without LiveData, my progect uses Coroutine Flow
I can't return a result of viewModelScope job in .run{} or by lazy {}.
I cant return a result drom fun:
val data: List<String> = getData()
fun getData(): List<String> {
viewModelScope.launch {
data = gettingData.get()
}
return ???
}
Also I can't make suspend fun getData() because I can't create coroutineScope in initialisation'
You're describing an impossibility. Presumably, gettingData.get() is defined as a suspend function, meaning the result literally cannot be retrieved immediately. Since it takes a while to retrieve, you cannot have an immediate value.
This is why apps and websites have loading indicators in their UI.
If you're using Flows, you can use a Flow with a nullable type (like in your option 2 above), and in your Activity/Fragment, in the collector, you show either a loading indicator or your data depending on whether it is null.
Your code 2 can be simplified using the flow builder and stateIn with a null default value:
class SomeViewModel(
private val gettingData: GetDataUseCase
) : ViewModel() {
val data = flow<List<String>?> { emit(gettingData.get()) }
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
}
In your Activity or Fragment:
viewLifecycleOwner.lifecycleScope.launch {
viewModel.data
.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.collect { list ->
if(list == null) {
// Show loading indicator in UI
} else {
// Show the data
}
}
}
If your data loads pretty quickly, instead of making the type nullable, you can just make the default value emptyList(). Then your collector can just not do anything when the list is empty. This works if the data loads quickly enough that the user isn't going to wonder if something is wrong because the screen is blank for so long.
You have to use SharedFlow with replay 1 (to store last value and replay it for a new subscriber) to implement it.
My sample:
interface DataSource {
suspend fun getData(): Int
}
class DataViewModel(dataSource: DataSource): ViewModel() {
val dataField =
flow<Int> {
emit(dataSource.getData())
}.shareIn(viewModelScope, SharingStarted.WhileSubscribed(1000), 1)
}

one-shot operation with Flow in Android

I'm trying to show a user information in DetailActivity. So, I request a data and get a data for the user from server. but in this case, the return type is Flow<User>. Let me show you the following code.
ServiceApi.kt
#GET("endpoint")
suspend fun getUser(#Query("id") id: Int): Response<User>
Repository.kt
fun getUser(id: Int): Flow<User> = flow<User> {
val userResponse = api.getUser(id = id)
if (userResponse.isSuccessful) {
val user = userResponse.body()
emit(user)
}
}
.flowOn(Dispatchers.IO)
.catch { // send error }
DetailViewModel.kt
class DetailViewModel(
private val repository : Repository
) {
val uiState: StateFlow<User> = repository.getUser(id = 369).stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = User() // empty user
)
}
DetailActivity.kt
class DetailActivity: AppCompatActivity() {
....
initObersevers() {
lifecycleScope.launch {
// i used the `flowWithLifecycle` because the data is just a single object.
viewModel.uiState.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED).collect { state ->
// show data
}
}
}
...
}
But, all of sudden, I just realized that this process is just an one-shot operation and thought i can use suspend function and return User in Repository.kt.
So, i changed the Repository.kt.
Repository.kt(changed)
suspend fun getUser(id: Int): User {
val userResponse = api.getUser(id = id)
return if(userResponse.isSuccessful) {
response.body()
} else {
User() // empty user
}
}
And in DetailViewModel, i want to convert the User into StateFlow<User> because of observing from DetailActivity and I'm going to use it the same way as before by using flowWithLifecycle.
the concept is... i thought it's just one single data and i dind't need to use Flow in Repository. because it's not several items like List.
is this way correct or not??
Yeap, this one-time flow doesn't make any sense - it emits only once and that's it.
You've got two different ways. First - is to create a state flow in your repo and emit there any values each time you're doing your GET request. This flow will be exposed to the use case and VM levels. I would say that it leads to more difficult error handling (I'm not fond of this way, but these things are always arguable, haha), but it also has some pros like caching, you can always show/get the previous results.
Second way is to leave your request as a simple suspend function which sends a request, returns the result of it back to your VM (skipping error handling here to be simple):
val userFlow: Flow<User>
get() = _userFlow
private val _userFlow = MutableStateFlow(User())
fun getUser() = launch(viewModelScope) {
_userFlow.value = repository.getUser()
}
This kind of implementation doesn't provide any cache out of scope of this VM's lifecycle, but it's easy to test and use.
So it's not like there is only one "the-coolest-way-to-do-it", it's rather a question what suits you more for your needs.

How to get LiveData to switch between two other LiveData

I have the following scenario. Podcasts can come from internet or local(db) both are LiveData
// Live
private val _live = MutableLiveData<List<Podcast>>()
val live: LiveData<List<Podcast>> = _live
// Local
val local: LiveData<List<Podcast>> = dao.observePodcasts()
// Combined
val podcasts: LiveData<List<Podcast>> = ...
My question is:- How can i use only one LiveData podcasts such that on demand I can get data from live or local
fun search(query: String) {
// podcasts <- from live
}
fun subcribed() {
// podcasts <- from local
}
You can use MediatorLiveData in this case.
What you need to do with MediatorLiveData is need the LiveData sources to be able to listen for changes to the LiveData source.
Try the following:
YourViewModel.kt
private val _podcasts = MediatorLiveData<List<Podcast>>().apply {
addSource(_live) { dataApi ->
// Or you can do something when `_live` has a change in value.
if(local.value == null) {
this.value = dataApi
}
}
addSource(local) { dataLocal ->
// Or you can do something when `local` has a change in value.
if(_live.value == null) {
this.value = dataLocal
}
}
}
val podcasts: LiveData<List<Podcast>> = _podcasts
MediatorLiveData
I've personally used MediatorLiveData in projects to achieve the same function you're describing.
As quoted directly from the docs since they are pretty straight forward...
Consider the following scenario: we have 2 instances of LiveData, let's name them liveData1 and liveData2, and we want to merge their emissions in one object: liveDataMerger. Then, liveData1 and liveData2 will become sources for the MediatorLiveData liveDataMerger and every time onChanged callback is called for either of them, we set a new value in liveDataMerger.
LiveData liveData1 = ...;
LiveData liveData2 = ...;
MediatorLiveData liveDataMerger = new MediatorLiveData<>();
liveDataMerger.addSource(liveData1, value -> liveDataMerger.setValue(value));
liveDataMerger.addSource(liveData2, value -> liveDataMerger.setValue(value));
As already suggested, this can be accomplished with MediatorLiveData. Another option would be using Flows instead of combining LiveData.
val podcasts = combine(local, live) { local, live ->
// Add your implementation of how you would like to combine them
live ?: local
}.asLiveData(viewModelScope.coroutineContext)
If you're using Room, you can simply change the return type to Flow to get a Flow result. And for the MutableLiveData you can replace it with MutableStateFlow.
Using MediatorLiveData didn't suit my needs as I expected because I wanted to be able to switch between local and live whenever I want!
So I did the implementation as follows
enum class Source {
LIVE, LOCAL
}
private val _live = MutableLiveData<List<Podcast>>()
private val _local = dao.observePodcasts()
private val source = MutableLiveData<Source>(Source.LOCAL)
// Universal
val podcasts: LiveData<List<Podcasts>> = source.switchMap {
liveData {
when (it) {
Source.LIVE -> emitSource(_live)
else -> emitSource(_local)
}
}
}
emitSource() removes the previously-added source.
Then I implemented the following two methods
fun goLocal() {
source.postValue(Source.LOCAL)
}
fun goLive() {
source.postValue(Source.LIVE)
}
I then call respected function whenever to observer from live or local storage
One of the usecase
searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(p0: MenuItem?): Boolean {
viewModel.goLive()
return true
}
override fun onMenuItemActionCollapse(p0: MenuItem?): Boolean {
viewModel.goLocal()
return true
}
})

liveData with coroutines only trigger first time

I have a usecase:
Open app + disable network -> display error
Exit app, then enable network, then open app again
Expected: app load data
Actual: app display error that meaning state error cached, liveData is not emit
Repository class
class CategoryRepository(
private val api: ApiService,
private val dao: CategoryDao
) {
val categories: LiveData<Resource<List<Category>>> = liveData {
emit(Resource.loading(null))
try {
val data = api.getCategories().result
dao.insert(data)
emit(Resource.success(data))
} catch (e: Exception) {
val data = dao.getCategories().value
if (!data.isNullOrEmpty()) {
emit(Resource.success(data))
} else {
val ex = handleException(e)
emit(Resource.error(ex, null))
}
}
}
}
ViewModel class
class CategoryListViewModel(
private val repository: CategoryRepository
): ViewModel() {
val categories = repository.categories
}
Fragment class where LiveDate obsever
viewModel.apply {
categories.observe(viewLifecycleOwner, Observer {
// live data only trigger first time, when exit app then open again, live data not trigger
})
}
can you help me explain why live data not trigger in this usecase and how to fix? Thankyou so much
Update
I have resolved the above problem by replace val categories by func categories() at repository class. However, I don't understand and can't explain why it works properly with func but not val.
Why does this happen? This happens because your ViewModel has not been killed yet. The ViewModel on cleared() is called when the Fragment is destroyed. In your case your app is not killed and LiveData would just emit the latest event already set. I don't think this is a case to use liveData builder. Just execute the method in the ViewModel when your Fragment gets in onResume():
override fun onResume(){
viewModel.checkData()
super.onResume()
}
// in the viewmodel
fun checkData(){
_yourMutableLiveData.value = Resource.loading(null)
try {
val data = repository.getCategories()
repository.insert(data)
_yourMutableLiveData.value = Resource.success(data)
} catch (e: Exception) {
val data = repository.getCategories()
if (!data.isNullOrEmpty()) {
_yourMutableLiveData.value = Resource.success(data)
} else {
val ex = handleException(e)
_yourMutableLiveData.value = Resource.error(ex,null)
}
}
}
Not sure if that would work, but you can try to add the listener directly in onResume() but careful with the instantiation of the ViewModel.
Small advice, if you don't need a value like in Resource.loading(null) just use a sealed class with object
UPDATE
Regarding your question that you ask why it works with a function and not with a variable, if you call that method in onResume it will get executed again. That's the difference. Check the Fragment or Activity lifecycle before jumping to the ViewModel stuff.

Communication between view and ViewModel in MVVM with LiveData

What is a proper way to communicate between the ViewModel and the View, Google architecture components give use LiveData in which the view subscribes to the changes and update itself accordingly, but this communication not suitable for single events, for example show message, show progress, hide progress etc.
There are some hacks like SingleLiveEvent in Googles example but it work only for 1 observer.
Some developers using EventBus but i think it can quickly get out of control when the project grows.
Is there a convenience and correct way to implement it, how do you implement it?
(Java examples welcome too)
Yeah I agree, SingleLiveEvent is a hacky solution and EventBus (in my experience) always lead to trouble.
I found a class called ConsumableValue a while back when reading the Google CodeLabs for Kotlin Coroutines, and I found it to be a good, clean solution that has served me well (ConsumableValue.kt):
class ConsumableValue<T>(private val data: T) {
private var consumed = false
/**
* Process this event, will only be called once
*/
#UiThread
fun handle(block: ConsumableValue<T>.(T) -> Unit) {
val wasConsumed = consumed
consumed = true
if (!wasConsumed) {
this.block(data)
}
}
/**
* Inside a handle lambda, you may call this if you discover that you cannot handle
* the event right now. It will mark the event as available to be handled by another handler.
*/
#UiThread
fun ConsumableValue<T>.markUnhandled() {
consumed = false
}
}
class MyViewModel : ViewModel {
private val _oneShotEvent = MutableLiveData<ConsumableValue<String>>()
val oneShotEvent: LiveData<ConsumableValue<String>>() = _oneShotData
fun fireEvent(msg: String) {
_oneShotEvent.value = ConsumableValue(msg)
}
}
// In Fragment or Activity
viewModel.oneShotEvent.observe(this, Observer { value ->
value?.handle { Log("TAG", "Message:$it")}
})
In short, the handle {...} block will only be called once, so there's no need for clearing the value if you return to a screen.
What about using Kotlin Flow?
I do not believe they have the same behavior that LiveData has where it would alway give you the latest value. Its just a subscription similar to the workaround SingleLiveEvent for LiveData.
Here is a video explaining the difference that I think you will find interesting and answer your questions
https://youtu.be/B8ppnjGPAGE?t=535
try this:
/**
* Used as a wrapper for data that is exposed via a LiveData that represents an event.
*/
open class Event<out T>(private val content: T) {
var hasBeenHandled = false
private set // Allow external read but not write
/**
* Returns the content and prevents its use again.
*/
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
/**
* Returns the content, even if it's already been handled.
*/
fun peekContent(): T = content
}
And wrapper it into LiveData
class ListViewModel : ViewModel {
private val _navigateToDetails = MutableLiveData<Event<String>>()
val navigateToDetails : LiveData<Event<String>>
get() = _navigateToDetails
fun userClicksOnButton(itemId: String) {
_navigateToDetails.value = Event(itemId) // Trigger the event by setting a new Event as a new value
}
}
And observe
myViewModel.navigateToDetails.observe(this, Observer {
it.getContentIfNotHandled()?.let { // Only proceed if the event has never been handled
startActivity(DetailsActivity...)
}
})
link reference: Use an Event wrapper
For showing/hiding progress dialogs and showing error messages from a failed network call on loading of the screen, you can use a wrapper that encapsulates the LiveData that the View is observing.
Details about this method are in the addendum to app architecture:
https://developer.android.com/jetpack/docs/guide#addendum
Define a Resource:
data class Resource<out T> constructor(
val state: ResourceState,
val data: T? = null,
val message: String? = null
)
And a ResourceState:
sealed class ResourceState {
object LOADING : ResourceState()
object SUCCESS : ResourceState()
object ERROR : ResourceState()
}
In the ViewModel, define your LiveData with the model wrapped in a Resource:
val exampleLiveData = MutableLiveData<Resource<ExampleModel>>()
Also in the ViewModel, define the method that makes the API call to load the data for the current screen:
fun loadDataForView() = compositeDisposable.add(
exampleUseCase.exampleApiCall()
.doOnSubscribe {
exampleLiveData.setLoading()
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
exampleLiveData.setSuccess(it)
},
{
exampleLiveData.setError(it.message)
}
)
)
In the View, set up the Observer on creation:
viewModel.exampleLiveData.observe(this, Observer {
updateResponse(it)
})
Here is the example updateResponse() method, showing/hiding progress, and showing an error if appropriate:
private fun updateResponse(resource: Resource<ExampleModel>?) {
resource?.let {
when (it.state) {
ResourceState.LOADING -> {
showProgress()
}
ResourceState.SUCCESS -> {
hideProgress()
// Use data to populate data on screen
// it.data will have the data of type ExampleModel
}
ResourceState.ERROR -> {
hideProgress()
// Show error message
// it.message will have the error message
}
}
}
}
You can easily achieve this by not using LiveData, and instead using Event-Emitter library that I wrote specifically to solve this problem without relying on LiveData (which is an anti-pattern outlined by Google, and I am not aware of any other relevant alternatives).
allprojects {
repositories {
maven { url "https://jitpack.io" }
}
}
implementation 'com.github.Zhuinden:event-emitter:1.0.0'
If you also copy the LiveEvent class , then now you can do
private val emitter: EventEmitter<String> = EventEmitter()
val events: EventSource<String> get() = emitter
fun doSomething() {
emitter.emit("hello")
}
And
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel = getViewModel<MyViewModel>()
viewModel.events.observe(viewLifecycleOwner) { event ->
// ...
}
}
// inline fun <reified T: ViewModel> Fragment.getViewModel(): T = ViewModelProviders.of(this).get(T::class.java)
For rationale, you can check out my article I wrote to explain why the alternatives aren't as valid approaches.
You can however nowadays also use a Channel(UNLIMITED) and expose it as a flow using asFlow(). That wasn't really applicable back in 2019.

Categories

Resources