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)
}
Related
I have this MutableStateFlow<>() declaration:
private val _books = MutableStateFlow<List<Book>>(emptyList())
I am trying to append/add results from the database:
fun fetchAllBooks(user_id: Long) = viewModelScope.launch(Dispatchers.IO) {
dbRepository.getAllUsersBooks(user_id).collect{ books ->
_books.add() // Does not exist, nor does the 'postValue' method exists
}
}
But, this does not work as I though, non of the expected methods exists.
If you need to update the state of a MutableStateFlow, you can set the value property:
fun fetchAllBooks(user_id: Long) = viewModelScope.launch(Dispatchers.IO) {
dbRepository.getAllUsersBooks(user_id).collect{ books ->
_books.value = books
}
}
It will trigger events on collectors if the new value is different from the previous one.
But if getAllUsersBooks already returns a Flow<List<Book>>, you could also simply use it directly instead of updating a state flow.
If you really want a StateFlow, you can also use stateIn:
fun fetchAllBooks(user_id: Long) = dbRepository.getAllUsersBooks(user_id)
.flowOn(Dispatchers.IO) // likely unnecessary if your DB has its own dispatcher anyway
.stateIn(viewModelScope)
You are actually declaring the immutable list and trying to add and remove data instade of that use mutable list to add or remove data from list like here :-
private var _bookState = MutableStateFlow<MutableList<Book>>(mutableListOf())
private var books=_bookState.asStateFlow()
var bookList= books.value
and to send the data to the state use this:-
viewModelScope.launch {
_bookState.value.add(BookItem)
}
viewModelScope.launch {
_bookState.value.remove(BookItem)
}
I hope this will work out for you if you have any query pls tell me in comment.
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.
I have the following ViewModel with MutableLiveData data and another LiveData ones that is derived from data in a way that it updates its value only if the data.number is equal to 1.
class DummyViewModel : ViewModel() {
private val data = MutableLiveData<Dummy>()
val ones = data.mapNotNull { it.takeIf { it.number == 1 } }
init {
data.value = Dummy(1, "Init")
doSomething()
}
fun doSomething() {
data.value = Dummy(2, "Do something")
}
}
data class Dummy(val number: Int, val text: String)
fun <T, Y> LiveData<T>.mapNotNull(mapper: (T) -> Y?): LiveData<Y> {
val mediator = MediatorLiveData<Y>()
mediator.addSource(this) { item ->
val mapped = mapper(item)
if (mapped != null) {
mediator.value = mapped
}
}
return mediator
}
I observe ones in my fragment. However, If I execute doSomething, I don't receive any updates in my fragment. If I don't execute doSomething, the dummy Init is correctly present in ones and I receive an update.
What is happening here? Why is ones empty and how can I overcome this issue?
Maybe I'm missing something, but the behavior seems like expected to me...
Lets' try to reproduce both cases sequentially.
Without doSomething() :
Create Livedata
Add Dummy(1, "Init")
Start listening in the fragment: Because number is 1, it passes your filter and the fragment receives it
With doSomething():
Create Livedata
Add Dummy(1, "Init")
Add Dummy(2, "Do something") (LiveData keeps only the last value, so if nobody observes, the first value is getting lost)
Start listening in the fragment: Because number is 2, the value gets filtered and the fragment receives nothing
A little offtopic: it's always good to write tests for ViewModel cases like this, because you'll be able to isolate the problem and find the real reason quickly.
EDIT: also be aware that your filter is only working on observing, it isn't applied when putting the value into LiveData.
I will hope that when i call to "addPlantToGarden()" passing respect "plantId" parameter then fire the "observers" "Transformations.switchMap(plantName)" but that doesn't happen, what is the error?
private val plantName: MutableLiveData<String> = MutableLiveData()
val plant: LiveData<Plant> = Transformations.switchMap(plantName){plantId ->
plantRepository.getPlant(plantId)
}
val isPlanted: LiveData<Boolean> = Transformations.switchMap(plantName){plantId ->
gardenPlantingRepository.isPlanted(plantId)
}
fun addPlantToGarden(plantId: String) {
plantName.value = plantId
}
These are a few things to consider:
1. Check your Repository
Make sure your plantRepository.getPlant(plantId) returns LiveData. Since methods from Repository are executed in background, I prefer encapsulate the function using this:
liveData {
// some async process (e.g. HTTP Request)
emit(/*your value*/)
}
Reference: https://developer.android.com/topic/libraries/architecture/coroutines#livedata
2. Check your Observer
Are you observing on a correct view lifecycle owner? If your ViewModel is inside a Fragment, make sure to do this:
viewModel.plant.observe(viewLifecycleOwner, Observer{
// action
})
instead of:
viewModel.plant.observe(this, Observer{
// action
})
And make sure to observe first before trying to change your plantName value.
3. Start with a simple case
I have no idea how you changed your plantName value. But try from a simple hardcoded/mock value first, for example:
plantName.value = "1"
then trace it through your Repository, then down to your Observer. Hopefully this will help you.
In my fragment I observe dbQuestionsList field:
viewModel.dbQuestionsList.observe(viewLifecycleOwner, Observer { list ->
Log.i("a", "dbQuestionsList inside fragment = $list ")
})
In my fragment I have few buttons and depending on which one is pressed I call method on viewModel passing the string which was set as tag to the button.
viewModel.onFilterChanged(button.tag as String)
My ViewMode:
lateinit var dbQuestionsList: LiveData<List<DatabaseQuestion>>
init{
onFilterChanged("")
}
private fun onFilterChanged(filter: String) {
dbQuestionsList = mRepository.getDbQuestionsForCategory(filter)
}
Repository method:
fun getDbQuestionsForCategory(categoryName: String): LiveData<List<DatabaseQuestion>> {
return database.dbQuestionsDao().getDbQuestionsByCategory(categoryName)
}
Dao method:
#Query("SELECT * FROM db_questions_database WHERE category = :categoryId")
fun getDbQuestionsByCategory(categoryId: String): LiveData<List<DatabaseQuestion>>
When I press button, viewModel method is called with argument which should be used to update LiveData by searching through room database, but NOTHING gets updated for no reason. Database is not empty so there is no reason to return null and not trigger observer in main Fragment.
But when I do this in my viewModel:
lateinit var dbQuestionsList: LiveData<List<DatabaseQuestion>>
init{
onFilterChanged("travel")
}
where I hardcode parameter, the room will return list and observer in fragment will be triggered, so it works like that but doesn't work when arguments is passed when button is pressed, Please explain because this thing doesn't make sense. I tried with mutable live data, with using .setValue and .postValue but NOTHING works.
The reason you aren't getting updates is because onFilterChanged() is reassigning dbQuestionsList, not updating it. So the variable you observe initially is never actually modified.
I would probably implement this using a Transformation:
val filter = MutableLiveData<String>().apply { value = "" }
val dbQuestionsList = Transformations.switchMap(filter) {
mRepository.getDbQuestionsForCategory(filter)
}
Then in your fragment just set the filter when your button is clicked:
viewModel.filter.value = button.tag as String
Try this:
dbQuestionsList.value = mRepository.getDbQuestionsForCategory(filter)
or
dbQuestionsList.postValue(mRepository.getDbQuestionsForCategory(filter))