JanasC12
JanasC12

Reputation: 93

How to call again LiveData Coroutine Block

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.

  1. I navigate to a fragment
  2. User's data loads
  3. I click delete btn while being in the same fragment
  4. 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.

Upvotes: 8

Views: 4390

Answers (5)

rosuh
rosuh

Reputation: 483

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()
    }
}


Upvotes: 2

szsoftware
szsoftware

Reputation: 23

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.

Upvotes: 0

Md. Yamin Mollah
Md. Yamin Mollah

Reputation: 1801

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.

Upvotes: 0

Arif Nadeem
Arif Nadeem

Reputation: 8604

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.

Upvotes: -1

Circusmagnus
Circusmagnus

Reputation: 66

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.

Upvotes: 2

Related Questions