Kotlin - combining flows - android

In my viewModel I have:
private val setEntitiesList = mutableStateListOf<Exercise>()
val exercisesFromDB = exerciseDao.getAllExercisesWithSetNo(trainingId)
val exercises =
exercisesFromDB.combine(setEntitiesList.asFlow()) { exercises, setEntitiesList ->
Pair(exercises, setEntitiesList)
}.mapLatest { (exercises, setEntitiesList) ->
//I am altering exercises list here
exercises
}
In my fragment I have:
lifecycleScope.launchWhenStarted {
repeatOnLifecycle(Lifecycle.State.STARTED){
addTrainingViewModel.exercises.collectLatest {
exercisesAdapter.submitList(it)
}
}
}
It doesnt work. Nothing is collected in fragment
If I change in fragment to collect "addTrainingViewModel.exercisesFromDB" it works - values are emited and collected.
What I would like to achieve: new list of exercises is emitted when setEntitiesList or exercisesFromDB are changed and I am able to do sth with a list of exercises before it is emitted

I assume that setEntitiesList variable is of type Iterable. In that case when you convert it to Flow like setEntitiesList.asFlow() you create a cold Flow, it means that when you add new elements to the setEntitiesList iterable they won't be emitted to the combined Flow.
On the other hand, by calling addTrainingViewModel.exercisesFromDB you get a hot Flow, so when you tested it separately you got the right result.
So you can't use a converted iterable object setEntitiesList as hot Flow. You need somehow to rewrite your logic and use a hot Flow instead of cold Flow. You can use MutableSharedFlow for that. The code will be something like the following:
private val setEntitiesListFlow = MutableSharedFlow<Exercise>(extraBufferCapacity = 64)
val exercisesFromDB = exerciseDao.getAllExercisesWithSetNo(trainingId)
val exercises =
exercisesFromDB.combine(setEntitiesListFlow) { exercises, setEntitiesList ->
Pair(exercises, setEntitiesList)
}.mapLatest { (exercises, setEntitiesList) ->
//I am altering exercises list here
exercises
}
// somewhere else in your code emitting Exercise:
setEntitiesListFlow.tryEmit(Exercise(...))

Related

update a MutableStateFlow when another MutableStateFlow emits

val persons = MutableStateFlow<List<Person>>(emptyList())
val names = MutableStateFlow<List<String>>(emptyList())
I want to update names whenever persons emits a new value.
This could be done by observing persons like:
viewModelScope.launch{
persons.collectLatest{personList->
names.emit(personList.map{it.name})
}
}
but I was wondering if there is another way to achieve that, e.g. using flow operators ?
Looks a little nicer
persons.map{ persons ->
names.emit(persons.map{ it.name })
}.launchIn(viewModelScope)
If there is no need for reactive actions, then a function can be used.
val names = {
persons.map{
it.name
}
}
// call
println(names())
If it's a class property, even better
val names: String
get() = persons.map{it.name}

LiveData - How to observe changes to List inside object?

I have a Composable, a ViewModel and an object of a User class with a List variable in it. Inside the ViewModel I define a LiveData object to hold the User object and in the Composable I want to observe changes to the List inside the User object but it doesn't seem to work very well.
I understand when you change the contents of a List its reference is the same so the List object doesn't change itself, but I've tried copying the list, and it doesn't work; copying the whole User object doesn't work either; and the only way it seems to work is if I create a copy of both. This seems too far-fetched and too costly for larger lists and objects. Is there any simpler way to do this?
The code I have is something like this:
Composable
#Composable
fun Greeting(viewModel: ViewModel) {
val user = viewModel.user.observeAsState()
Column {
// TextField and Button that calls viewModel.addPet(petName)
LazyColumn {
items(user.value!!.pets) { pet ->
Text(text = pet)
}
}
}
}
ViewModel
class ViewModel {
val user: MutableLiveData<User> = MutableLiveData(User())
fun addPet(petName: String){
val sameList = user.value!!.pets
val newList = user.value!!.pets.toMutableList()
newList.add(petName)
sameList.add(petName) // This doesn't work
user.value = user.value!!.copy() // This doesn't work
user.value!!.pets = newList // This doesn't work
user.value = user.value!!.copy(pets = newList) // This works BUT...
}
}
User
data class User(
// Other variables
val pets: MutableList<String> = mutableListOf()
)
MutableLiveData will only notify view when it value changes, e.g. when you place other value which is different from an old one. That's why user.value = user.value!!.copy(pets = newList) works.
MutableLiveData cannot know when one of the fields was changed, when they're simple basic types/classes.
But you can make pets a mutable state, in this case live data will be able to notify about changes. Define it like val pets = mutableStateListOf<String>().
I personally not a big fan of live data, and code with value!! looks not what I'd like to see in my project. So I'll tell you about compose way of doing it, in case your project will allow you to use it. You need to define both pets as a mutable state list of strings, and user as a mutable state of user.
I suggest you read about compose states in the documentation carefully.
Also note that in my code I'm defining user with delegation, and pets without delegation. You can use delegation only in view model, and inside state holders you cannot, othervise it'll become plain objects at the end.
#Composable
fun TestView() {
val viewModel = viewModel<TestViewModel>()
Column {
// TextField and Button that calls viewModel.addPet(petName)
var i by remember { mutableStateOf(0) }
Button(onClick = { viewModel.addPet("pet ${i++}") }) {
Text("add new pet")
}
LazyColumn {
items(viewModel.user.pets) { pet ->
Text(text = pet)
}
}
}
}
class User {
val pets = mutableStateListOf<String>()
}
class TestViewModel: ViewModel() {
val user by mutableStateOf(User())
fun addPet(petName: String) {
user.pets.add(petName)
}
}
Jetpack Compose works best with immutable objects, making a copy with modern Android and ART is not the issue that it was in the past.
However, if you do not want to make a whole copy of your object, you could add a dummy int to it and then mutate that int when you also mutate the list, but I strongly urge you to consider immutability and instantiate a new User object instead.

Jetpack compose with RxJava2 and Realm

I am working my through a new android application using Jetpack Compose (1.0.0-alpha08) and RxJava2 to manage the flow of data from my model (Realm 10 in this case. For a given screen, I have a view model that defines the data that will be subscribed to by the top level Compostable view. So, for example:
ViewModel...
class ListItemViewModel: ViewModel() {
val items: Flowable<Item>
get() {
val data1 = userRealm.where<Item1>()
.also(query).findAllAsync().asFlowable()
.onBackpressureLatest().doOnNext{System.out.println("Realm on Next")}
.observeOn(
Schedulers.single()
).filter{it.isLoaded}.map{ result ->
System.out.println("Maping Realm")
result
}.doOnSubscribe {System.out.println("Subscribe")}
val data2 == //same as above but with a different item
return Flowable.combineLatest(data1, data2, combineFunction)
.onBackpressureLatest()
.doOnNext{System.out.println("Hello")}
.doOnComplete {System.out.println("Complete")}
.subscribeOn(AndroidSchedulers.mainThread())
}
}
View
#Compostable
fun List(List<Item> items) {
val viewModel: ListItemViewModel = viewModel()
val list by viewModel.items.subscribeAsState(initial = listOf())
ItemList(list = list)
}
#Compostable
fun ItemList(List<Item> items {
LazyColumnFor(...) {
.......
}
}
Everything works as I would expect and the list renders on the screen as I want. However, what I assume would happen here is that the subscribe would only happen once and the Flowable would only push out new data as new data was emitted. As a result, I would only expect the various onNext methods to be triggered when new data was present in the stream, e.g. something changed in the realm db. As I am not adding/deleting any data to/from the Realm, once I have the first set of results, I would expect the flowable to go "silent".
However, when I run the above, the subscribe message related to the realm subscription is logged over and over. The same for the "Hello" and the other logging statements in the onNext methods. Also, if I add any logging in my combine function, I see those log statements in the same fashion as I see the "Hello" log. From this it seems like each time the List composable is being rendered, it resubscribes to the Flowable from my viewmodel and triggers the full process. As I said, I was expecting that this subscription would only happen once.
This is perhaps correct behaviour, but mentally, it feels like I am burning CPU cycles for no reason as my methods are being called over and over when no data has change. Am I setting things up correctly, or is there something flawed in how I have configured things?
I ultimately worked around the problem and took a hybrid approach where I used Realm/RXJava to handle the data flow and when things have changed, update a LiveData object.
View Model
private val internalItemList = MutableLiveData(listOf<Item>())
val itemList: LiveData<List<Item>> = internalItemList
//capture the subscription so you can dispose in onCleared()
val subscription = items.observeOn(AndroidSchedulers.mainThread()).subscribe {
this.internalItemList.value = it
}
View
val list by viewModel.itemList.observeAsState(listOf())
This is must less chatty and works as I want it to. Not sure if it is the correct way to do this, but it seems to be working

What is the difference between emit and emitSource with LiveData ? ( as in REAL time USE-CASE )

emit accepts the data class whereas emitSource accepts LiveData<T> ( T -> data ). Considering the following example :- I have two type of calls :-
suspend fun getData(): Data // returns directly data
and the other one ;
suspend fun getData(): LiveData<Data> // returns live data instead
For the first case i can use:-
liveData {
emit(LOADING)
emit(getData())
}
My question : Using the above method would solve my problem , WHY do we need emitSource(liveData) anyway ?
Any good use-case for using the emitSource method would make it clear !
As you mentioned, I don't think it solves anything in your stated problem, but I usually use it like this:
If I want to show cached data to the user from the db while I get fresh data from remote, with only emit it would look something like this:
liveData{
emit(db.getData())
val latest = webService.getLatestData()
db.insert(latest)
emit(db.getData())
}
But with emitSource it looks like this:
liveData{
emitSource(db.getData())
val latest = webService.getLatestData()
db.insert(latest)
}
Don't need to call emit again since the liveData already have a source.
From what I understand emit(someValue) is similar to myData.value = someValue whereas emitSource(someLiveValue) is similar to myData = someLiveValue. This means that you can use emit whenever you want to set a value once, but if you want to connect your live data to another live data value you use emit source. An example would be emitting live data from a call to room (using emitSource(someLiveData)) then performing a network query and emitting an error (using emit(someError)).
I found a real use-case which depicts the use of emitSource over emit which I have used many times in production now. :D The use-case:
Suppose u have some user data (User which has some fields like userId, userName ) returned by some ApiService.
The User Model:
data class User(var userId: String, var userName: String)
The userName is required by the view/activity to paint the UI. And the userId is used to make another API call which returns the UserData like profileImage , emailId.
The UserData Model:
data class UserData(var profileImage: String, var emailId: String)
This can be achieved internally using emitSource by wiring the two liveData in the ViewModel like:
User liveData -
val userLiveData: LiveData<User> = liveData {
emit(service.getUser())
}
UserData liveData -
val userDataLiveData: LiveData<UserData> = liveData {
emitSource(userLiveData.switchMap {
liveData {
emit(service.getUserData(it.userId))
}
})
}
So, in the activity / view one can ONLY call getUser() and the getUserData(userId) will be automatically triggered internally via switchMap.
You need not manually call the getUserData(id) by passing the id.
This is a simple example, imagine there is a chain of dependent-tasks which needs to be executed one after the other, each of which is observed in the activity. emitSource comes in handy
With emitSource() you can not only emit a single value, but attach your LiveData to another LiveData and start emitting from it. Anyway, each emit() or emitSource() call will remove the previously added source.
var someData = liveData {
val cachedData = dataRepository.getCachedData()
emit(cachedData)
val actualData = dataRepository.getData()
emitSource(actualData)
}
The activity that’s observing the someData object, will quickly receive the cached data on the device and update the UI. Then, the LiveData itself will take care of making the network request and replace the cached data with a new live stream of data, that will eventually trigger the Activity observer and update the UI with the updated info.
Source: Exploring new Coroutines and Lifecycle Architectural Components integration on Android
I will like share a example where we use "emit" and "emitsource" both to communicate from UI -> View Model -> Repository
Repository layer we use emit to send the values downstream :
suspend fun fetchNews(): Flow<Result<List<Article>>> {
val queryPath = QueryPath("tata", apikey = AppConstant.API_KEY)
return flow {
emit(
Result.success(
openNewsAPI.getResponse(
"everything",
queryPath.searchTitle,
queryPath.page,
queryPath.apikey
).articles
)
)
}.catch { exception ->
emit(Result.failure(RuntimeException(exception.message)));
}
}
ViewModel layer we use emitsource to pass the live data object to UI for subscriptions
val loader = MutableLiveData<Boolean>()
val newsListLiveData = liveData<Result<List<Article>>> {
loader.postValue(true)
emitSource(newRepo.fetchNews()
.onEach {
loader.postValue(false)
}
.asLiveData())
}
UI Layer - we observe the live data emitted by emitsource
viewModel.newsListLiveData.observe(viewLifecycleOwner, { result ->
val listArticle = result.getOrNull()
if (result.isSuccess && listArticle != null) {
setupList(binding.list, listArticle)
} else {
Toast.makeText(
appContext,
result.exceptionOrNull()?.message + "Error",
Toast.LENGTH_LONG
).show()
}
})
We convert Flow observable to LiveData in viewModel

How to chain transformations in Android when using live data?

Given the following setup:
I have 2 repositories: Repository A and Repository B both of them return live data.
I have a ViewModel that uses both of these repositories.
I want to extract something from Repository A and depending on the result I want to grab something from Repository B and then transform the result before returning to UI.
For this I have been looking at the LiveData Transformation classes. The examples show a single transformation of the result however I want something along the lines of chaining two transformations. How can I accomplish this?
I have tried setting something up like this but get a type mismatch on the second transformation block:
internal val launchStatus: LiveData<String> = Transformations
.map(respositoryA.getData(), { data ->
if (data.isValid){
"stringA"
} else {
//This gives a type mismatch for the entire block
Transformations.map(repositoryB.getData(), {
result -> result.toString()
})
}
})
(Also please let me know if there is an alternate/recommended approach for grabbing something for chaining these call i.e. grab something from A and then grab something from B depending on result of A and so on)
Your lambda sometimes returns the String "stringA", and sometimes returns the LiveData<String> given by:
Transformations.map(repositoryB.getData(), {
result -> result.toString()
})
This means that your lambda doesn't make sense - it returns different things in different branches.
As others have mentioned, you could write your own MediatorLiveData instead of using the one given by Transformations. However, I think it's easier to do the following:
internal val launchStatus: LiveData<String> = Transformations
.switchMap(respositoryA.getData(), { data ->
if (data.isValid) {
MutableLiveData().apply { setValue("stringA") }
} else {
Transformations.map(repositoryB.getData(), {
result -> result.toString()
})
}
})
All I've done is made the first code branch also return a LiveData<String>, so now your lambda makes sense - it's a (String) -> LiveData<String>. I had to make one other change: use switchMap instead of map. This is because map takes a lambda (X) -> Y, but switchMap takes a lambda (X) -> LiveData<Y>.
I used MediatorLiveData to solve this problem.
MediatorLiveData can observer other LiveData objects and react to them.
Instead of observing either of the repositories. I created myData (instance of MediatorLiveData) in my ViewModel class and have my view observe this object. Then I add Repository A as the initial source and observe that and only add Repository B if the result of A requires it. This allows me to keep the transformations associated with the live data of each of the repo and still process each result in the correct order. See below for implementation:
internal val myData: MediatorLiveData<String> = MediatorLiveData()
private val repoA: LiveData<String> = Transformations.map(
respositoryA.getData(), { data ->
if (data.isValid) "stringA" else ""
})
private val repoB: LiveData<String> = Transformations.map(
repositoryB.getData(), { data -> "stringB"
})
fun start() {
myData.addSource(repoA, {
if (it == "stringA") {
myData.value = it
} else {
myData.addSource(repoB, {
myData.value = it
})
}
})
}
Note: The solution does not cover the case where repoB might be added multiple times but it should be simple enough to handle.
I would try using switchMap instead of map:
Similar to map(), applies a function to the value stored in the LiveData object and unwraps and dispatches the result downstream. The function passed to switchMap() must return a LiveData object.
You can nest transformations.
val finalLiveData = Transformations.switchMap(liveData1){
val search = it
Transformations.switchMap(liveData2) {
db(context).dao().all(search, it)
}
}
You can transform the data by using switchmap. Here's an example documentation.

Categories

Resources