update a MutableStateFlow when another MutableStateFlow emits - android

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}

Related

Koltin Flow flatMapLatest to combineTransform Using Multiple StateFlows

I have the below working code which uses a dropdown to update the satusFilterFlow to allow for the filtering of characters through the getCharacterList call. The getCharacterList call uses the jetpack paging and returns Flow<PagerData<Character>>.
private val statusFilterFlow = MutableStateFlow<StatusFilter>(NoStatusFilter)
// private val searchFilterFlow = MutableStateFlow<SearchFilter>(NoSearchFilter)
val listData: LiveData<PagingData<Character>> =
statusFilterFlow.flatMapLatest{ statusFilter ->
characterRepository.getCharacterList(null, statusFilter.status)
.cachedIn(viewModelScope)
.flowOn(Dispatchers.IO)
}.asLiveData()
Given the above working solution, what is the correct flow extension to allow for me to add multiple StateFlows as I build out additional filters (e.g. SearchFilter).
I have tried combineTransorm as follows:
private val statusFilterFlow = MutableStateFlow<StatusFilter>(NoStatusFilter)
private val searchFilterFlow = MutableStateFlow<SearchFilter>(NoSearchFilter)
val listData: LiveData<PagingData<Character>> =
statusFilterFlow.combineTransform(searchFilterFlow) { statusFilter, searchFilter ->
characterRepository.getCharacterList(searchFilter.search, statusFilter.status)
.flowOn(Dispatchers.IO)
.cachedIn(viewModelScope)
}.asLiveData()
However, this gives me a "Not enough information to infer type variable R" error.
The usual way to understand and/or fix those errors is to specify types explicitly in the function call:
statusFilterFlow.combineTransform<StatusFilter, SearchFilter, PagingData<Character>>(searchFilterFlow) { ... }
This is orthogonal to the problem at hand, but I'd also suggest using the top-level combineTransform overload that takes all source flows as argument (instead of having the first one as receiver), so there is a better symmetry. Since I believe there is no reason one of the filters is more special than the other.
All in all, this gives:
val listData: LiveData<PagingData<Character>> =
combineTransform<StatusFilter, SearchFilter, PagingData<Character>>(statusFilterFlow, searchFilterFlow) { statusFilter, searchFilter ->
characterRepository.getCharacterList(searchFilter.search, statusFilter.status)
.flowOn(Dispatchers.IO)
.cachedIn(viewModelScope)
}.asLiveData()
For anymore else, this is too complex or doesn't work out for you ... Use Combine then flatMap latest on the top of that.
private val _selectionLocation: MutableStateFlow<Location?> = MutableStateFlow(null)
val searchKeyword: MutableStateFlow<String> = MutableStateFlow("")
val unassignedJobs: LiveData<List<Job>> =
combine(_selectionLocation, searchKeyword) { location: Location?, keyword: String ->
Log.e("HomeViewModel", "$location -- $keyword")
location to keyword
}.flatMapLatest { pair ->
_repo.getJob(Status.UNASSIGNED, pair.first).map {
Log.e("HomeViewModel", "size ${it.size}")
it.filter { it.desc.contains(pair.second) }
}
}.flowOn(Dispatchers.IO).asLiveData(Dispatchers.Main)

Kotlin - combining flows

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

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.

How can I perform Transformations on MutableLiveData?

The docs show how you can perform Transformations on a LiveData object? How can I perform a transformation like map() and switchMap() on a MutableLiveData object instead?
MutableLiveData is just a subclass of LiveData. Any API that accepts a LiveData will also accept a MutableLiveData, and it will still behave the way you expect.
Exactly the same way:
fun viewModelFun() = Transformations.map(mutableLiveData) {
//do somethinf with it
}
Perhaps your problem is you dont know how does yor mutable live data fit on this.
In the recent update mutable live data can start with a default value
private val form = MutableLiveData(Form.emptyForm())
That should trigger the transformation as soon as an observer is attached, because it will have a value to dispatch.
Of maybe you need to trigger it once the observer is attached
fun viewModelFun(selection: String) = liveData {
mutableLiveData.value = selection.toUpperCase
val source = Transformations.map(mutableLiveData) {
//do somethinf with it
}
emitSource(source)
}
And if you want the switch map is usually like this:
private val name = MutableLiveData<String>()
fun observeNames() = Transformations.switchMap(name) {
dbLiveData.search(name) //a list with the names
}
fun queryName(likeName: String) {
name.value = likeName
}
And in the view you would set a listener to the edit text of the search
searchEt.doAfterTextChange {...
viewModel.queryName(text)
}

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

Categories

Resources