Update value of LiveData everytime one of other LiveData updates its value - android

I have a LiveData which contains a List like so:
val originalSourceLiveaData = MutableLiveData<List<SomeType>>()
Now I have another LiveData which should indicate the filtering of the originalSourceLiveaData's value.
val filterLiveData = MutableLiveData<String>()
What I want is that everytime either one of those LiveData change value, a resulting list should be updated. I tried doing something like this:
val filteredListLiveData = MediatorLiveData<List<SomeType>().apply {
addSource(originalSourceLiveaData) { this.value = filteringMethod() }
addSource(filterLiveData) { this.value = filteringMethod() }
}
This works just fine but I wonder whether there is a better solution to this.
My issue is that if another LiveData is added I would have to add it as source like so:
val filteredListLiveData = MediatorLiveData<List<SomeType>().apply {
addSource(originalSourceLiveaData) { this.value = filteringMethod() }
addSource(filterLiveData) { this.value = filteringMethod() }
addSource(anotherSourceLiveData) {
this.value = filteringMethod() // this feels like a duplicate
}
}
Any ideas on improving this? Thanks in advance!

You can make it more reactive-style using the extension function feature of Kotlin.
Assume that you have firstLiveaData and secondLiveData with the same type of T. Now you want to filter them first and then listen to all of their changes.
So, you can add the following extension functions:
filter function will filter your livedata based on the given predicate function
addSources function will do the boilerplate of adding multiple livedata and listen to their changes
fun <T> LiveData<T>.filter(predicate : (T) -> Boolean): LiveData<T> {
val mutableLiveData = MediatorLiveData<T>()
mutableLiveData.addSource(this) {
if(predicate(it))
mutableLiveData.value = it
}
return mutableLiveData
}
fun <T> MediatorLiveData<T>.addSources(vararg listOfLiveData: LiveData<T>, callback: (T) -> Unit) {
listOfLiveData.forEach {
addSource(it, callback)
}
}
Also, you can merge multiple LivaData objects with the same type into one with merge function:
fun <T> merge(vararg liveDataList: LiveData<T>): LiveData<T> {
val mergedLiveData = MediatorLiveData<T>()
liveDataList.forEach { liveData ->
liveData.value?.let {
mergedLiveData.value = it
}
mergedLiveData.addSource(liveData) { source ->
mergedLiveData.value = source
}
}
return mergedLiveData
}
Here is an example:
fun doSomething() {
val firstLiveData = MutableLiveData<List<SomeType>>()
val secondLiveData = MutableLiveData<List<SomeType>>()
merge(firstLiveData, secondLiveData).filter { someFilterFunction() }.observe(...)
}
If you have a different type of LiveData (e.g. firstLiveData<Int> and secondLiveData<String>), you can simply add a map extension function.

Related

LiveData (not Flow) with states

I have a huge project and I need to refactor code to LiveData (not Flow). I have an Order and states in ViewModel. I cannot receive this Order in Activity when I observe it. How can I do this? This is my View Model:
private var _basicModel: MutableLiveData<OrderUiState> = MutableLiveData()
val basicModel: LiveData<OrderUiState> get() = _basicModel
sealed class OrderUiState {
object Loading : OrderUiState()
data class OrderFail(val message: String) : OrderUiState()
data class OrderSuccess(val order: Order) : OrderUiState()
}
fun getOrder(orderId: String) {
viewModelScope.launch {
_basicModel.value = OrderUiState.Loading
getOrderUseCase.execute(orderId, { order ->
_basicModel.value = OrderUiState.OrderSuccess(order)
}
}
And now I cannot to get to Order, when I have Succes Sate. My code want from me in Activity order, but I thought, that whan it is success, there it will be, but isn't?
viewModel.basicModel.observe(this) { order ->
when(order){
OrderViewModel.OrderUiState.OrderSuccess(here he want from me order... )
}
}
Can I get to order from this code?
You can do it this way:
viewModel.basicModel.observe(this) { uiState ->
when(uiState) {
is OrderViewModel.OrderUiState.OrderSuccess -> {
val order = uiState.order
// Use the order here
}
}
}

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

How to apply LiveData to xxxFragment.class instead of XML

How to apply live data isRefreshing to swiperefreshLayout.isRefresh?
Code in xxxViewModel.class:
var isRefreshing = MutableLiveData<Boolean>()
fun refresh() {
viewModelScope.launch(Dispatchers.Main) {
isRefreshing.value = true
withContext(Dispatchers.IO) {
fun doInBackground() // takes a long time
}
isRefreshing.value = false
}
}
Code in xxxFragment.class:
binding.swiperefreshLayout.setOnRefreshListener {
xxxViewModel.refresh()
}
// error here, require Boolean, found MutableLiveData<Boolean>
binding.swiperefreshLayout.isRefreshing = xxxViewModel.isRefreshing
Usually I apply the live data to XML like below, but I cannot find swiperefreshlayout.isRefreshing in XML.
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="#+id/swiperefresh_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
isFreshing = "#{xxxViewModel.isRefreshing}">
What you did is to get the current value of the liveData "isRefreshing". (also you need to add .value to get the underlying value associated with the liveData)
binding.swiperefreshLayout.isRefreshing = xxxViewModel.isRefreshing.value?:false
Instead, you should observe the Live Data for changes after initializing your view model.
xxxViewModel.apply {
isRefreshing.observe(viewLifecycleOwner){
binding.swiperefreshLayout.isRefreshing = it
}
//... observe the rest of your LiveData
}
EDIT:
you can create a function like the following
private inline fun <T> doObserve(ld: LiveData<T>, crossinline callback: (T) -> Unit) {
ld.observe(viewLifecycleOwner, { callback.invoke(it) })
}
then just call that instead
xxxViewModel.apply {
doObserve( isRefreshing){
binding.swiperefreshLayout.isRefreshing = it
}
//... observe the rest of your LiveData
}

How change MutableLiveData value inside ViewModel

I need to change the value of MutableLiveData in my ViewModel, but I can't make it because the value is equal to null, I think need to establish an observer change it inside that, but I don't know how to do it and whether it's a good idea.
AudioRecordersListViewModel
class AudioRecordersListViewModel() : ViewModel() {
var audioRecordsLiveData: MutableLiveData<MutableList<AudioRecordUI>> = MutableLiveData();
private var audioRecordDao: AudioRecordDao? = null
#Inject
constructor(audioRecordDao: AudioRecordDao) : this() {
this.audioRecordDao = audioRecordDao
viewModelScope.launch {
val liveDataItems = audioRecordDao
.getAll().value!!.map { item -> AudioRecordUI(item) }
.toMutableList()
if (liveDataItems.size > 0) {
liveDataItems[0].isActive = true
}
audioRecordsLiveData.postValue(liveDataItems)
}
}
}
AudioRecordDao
#Dao
interface AudioRecordDao {
#Query("SELECT * FROM AudioRecordEmpty")
fun getAll(): LiveData<MutableList<AudioRecordEmpty>>
}
First of all, using !! is not a good idea it can easily lead to NullPointer Exception, us ? instead.
You can set an empty list on your LiveData and add new data to that List:
var audioRecordsLiveData: MutableLiveData<MutableList<AudioRecordUI>> = MutableLiveData();
init {
audioRecordsLiveData.value = mutableListOf()
}
If you need to observe that LiveData:
mViewModel.mLiveData.observe(this, Observer { list ->
if (list.isNotEmpty()) {
//Update UI Stuff
}
})
never set your LiveData inside Fragment/Activity
If you need to update your LiveData:
mViewModel.onSomethingHappened()
Inside ViewModel:
fun onSomethingHappened() {
...
...
...
mLiveData.value = NEW_VALUE
}
If you want to update your LiveData from another thread use:
mLiveData.postValue()

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.
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.

Categories

Resources