I'm new to MVVM. I'm trying to figure out easiest way to change view from ViewModel. In fragment part I have navigation to next fragment
fun nextFragment(){
findNavController().navigate(R.id.action_memory_to_memoryEnd)
}
But I cannot call it from ViewModel. AFAIK it is not even possible and it destroys the conception of ViewModel.
I wanted to call fun nextFragment() when this condition in ViewModel is True
if (listOfCheckedButtonsId.size >= 18){
Memory.endGame()
}
Is there any simple way to change Views depending on values in ViewModel?
Thanks to Gorky's respond I figured out how to do that.
In Fragment I created observer
sharedViewModel.changeView.observe(viewLifecycleOwner, Observer<Boolean> { hasFinished ->
if (hasFinished) nextFragment()
})
I created changeView variable in ViewModel. When
var changeView = MutableLiveData<Boolean>()
change to true, observer call function.
source:
https://developer.android.com/codelabs/kotlin-android-training-live-data#6
Related
How can I directly get data from ViewModel in Activity or Fragment?
For example, I save data in ViewModel
private val _isPaypalPay = MutableLiveData(false)
val isPaypalPay: LiveData<Boolean>
get() = _isPaypalPay
And I want to use a logic in Activity.
R.id.nsbtn_pay_confirm -> {
if (!isDeliveryInfoFine()) return
if (!viewModel.isPaypalPay.value!! && !isCreditInfoFilledOut()) return
updateAndPay()
}
I have two options
viewModel.isPaypalPay.value!!
make a function that returns _isPaypalPay.value!!
fun getIsPaypalPay() = _isPaypalPay.value!!
Or is there any better way?
The whole point of livedata is that you can listen to changes in the fragment and that it will be lifecycleaware. If you are trying to directly access the data in livedata, you are doing it wrong.
Try reading here: https://developer.android.com/topic/libraries/architecture/livedata
It seems like recommended pattern for fields in viewmodel is:
val selected = MutableLiveData<Item>()
fun select(item: Item) {
selected.value = item
}
(btw, is it correct that the selected field isn't private?)
But what if I don't need to subscribe to the changes in the ViewModel's field. I just need passively pull that value in another fragment.
My project details:
one activity and a bunch of simple fragments replacing each other with the navigation component
ViewModel does the business logic and carries some values from one fragment to another
there is one ViewModel for the activity and the fragments, don't see the point to have more than one ViewModel, as it's the same business flow
I'd prefer to store a value in one fragment and access it in the next one which replaces the current one instead of pass it into a bundle and retrieve again and again manually in each fragment
ViewModel:
private var amount = 0
fun setAmount(value: Int) { amount = value}
fun getAmount() = amount
Fragment1:
bnd.button10.setOnClickListener { viewModel.setAmount(10) }
Fragment2:
if(viewModel.getAmount() < 20) { bnd.textView.text = "less than 20" }
Is this would be a valid approach? Or there is a better one? Or should I just use LiveData or Flow?
Maybe I should use SavedStateHandle? Is it injectable in ViewModel?
To answer your question,
No, It is not mandatory to use LiveData always inside ViewModel, it is just an observable pattern to inform the caller about updates in data.
If you have something which won't be changed frequently and can be accessed by its instance. You can completely ignore wrapping it inside LiveData.
And anyways ViewModel instance will be preserved and so are values inside it.
And regarding private field, MutableLiveData should never be exposed outside the class, as the data flow is always from VM -> View which is beauty of MVVM pattern
private val selected = MutableLiveData<Item>()
val selectedLiveData : LiveData<Item>
get() = selected
fun select(item: Item) {
selected.value = item
}
I have 3 LiveData objects in my ViewModel, I'm applying transformations to these, the problem is 2 LiveData are getting observed while the other one is not, I've tried different solutions like changing the way ViewModel is initialized or the way LiveData is initialized but nothing has worked for me.
class MyClass : ViewModel() {
init {
_originallist.value = Instance.getOrignalList()
}
// observed in Fragment A
val a: LiveData<List<A>> = Transformations.map(_searchText, ::extractA)
// observed in Fragment B
val b: LiveData<List<B>> = Transformations.map(_originallist, ::extractB)
// observed in Fragment C
val c: LiveData<List<C>> = Transformations.map(_originalList, ::extractC)
// Called from Fragment D to change the _searchText liveData
fun setSearchText(text: String) {
_searchText.value = text
}
fun extractA(text: String): List<A> {
val temp = ArrayList<A>()
list.foreach {
if (it.contains(text, false) temp . add (it)
}
return temp
}
fun extractB(list: List<B>): List<B> {
// do something
}
fun extractC(list: List<C>): List<C> {
// do something
}
}
If you have noticed that the LiveData b and c are getting initialized just once hence I'm able to see the data in my RecyclerView, but for the LiveData A, the search text can change based on user input, this is where my fragment is not observing this live data.
Things to note: This is a common ViewModel for my 3 viewPager fragments, LiveData a is observed in one fragment, B in another and C in another.
Eventually, I have to apply the search for other 2 fragments as well.
When I was debugging the observer lines in my fragment was getting skipped, another thing I would like to point out is that the code in all 3 fragments is same except that they are observing different LiveData
EDIT: What i have noticed now is that, since i'm calling the setSearchText() from Fragment D i'm able to observe the changes of LiveData A in Fragment D but i want to observe that in Fragment A but not able to.
I have a search bar in fragment D and bottom of that i have a view pager with 3 fragments, all 4 fragments have a common viewModel, Am i doing something wrong here or is this not the way to implement this?
TIA
Finally found the root cause, the problem was that the viewModel was getting its own lifecycle owner in each of fragment, the solution to this was to declare and initialize the viewModel object in the parent activity of the fragments and use its instace in the fragment to observe the LiveData
The problem is:
Your function extractA in
val a: LiveData<List<A>> = Transformations.map(_searchText, ::extractA)
will only be executed when the value of _searchText will change.
That's how Transformations work, they apply the given function whenever the value changes.
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.
First time implementing Room and Data Binding.
In my ViewModel:
var numberOfClubs = ObservableInt(0)
private var clubs: LiveData<List<Club>> = clubDao.getAllClubs()
I then try to get clubs.value after inserting into the Room database but am getting a KotlinNullPointerException every time.
fun addClubs() {
Observable.fromCallable({
clubDao.insertClubs(listOf(Club(null, "clubName"))
})
.subscribeOn(schedulerFacade.computation)
.subscribe(
{
numberOfClubs.set(clubs.value!!.size) <--- Kotlin NPE
},
{ error ->
logWrapper.e("MainViewModel", error.message!!)
clubError.set(true)
})
}
In my XML I'm calling: android:text="#{viewModel.numberOfClubs}".
As inserting into the Room database doesn't cause anything to be returned, I can't set numberOfClubs with the result. I assumed that clubs should track the changes?
Yes, as you figured out already LiveData is lifecycle aware. That means that it observe aslong as the livecycle owner is present.
Even if you've posted the solution already, you shouldnt use !! at all.
viewModel.clubs.observe(this, {
it?.let {
viewModel.numberOfClubs.set(it.size)
}
)
is the proper way doing a null check before assigning the size of your recieved list.
By the way, if you want to get the items size in your layout, you should use
text="#{viewModel.clubsList.size() + ``}"
and observe a list, not a list size using
val clubsList = ObservableArrayList<..>()
viewModel.clubs.observe(this, {
it?.let {
viewModel.clubsList.set(it)
}
)
Was a bit of a LiveData misunderstanding from myself. In order to get a LiveData object to begin emitting things is to .observe() it. As LiveData is lifecycle aware, .observe() is best called from within a Fragment or Activity as it has a reference to LifecycleOwner which must be the first parameter passed into .observe().
I'm perhaps not following best practices as I set an ObservableInt in the ViewModel from my Activity but I used:
viewModel.clubs
.observe(this, Observer { clubs ->
viewModel.numberOfClubs.set(clubs!!.size)
})