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.
Related
I pass the data through the Bundle to the `vm, I look at everything clearly in the log, but when I try to turn arguments in the fragment where I should have received it, null comes.
I can't enter at what point I could have lost them.
Here is my first vm:
fun sendData(it: ConfigurationDto?) {
Log.d("some", "sendData : $it")
if (it != null) {
OnboardingViewModel.newBundle(it)
}
}
Here is code of my OnboardingViewModel:
companion object {
val Bundle.configuration: ConfigurationDto?
get() = getSerializable("configuration") as? ConfigurationDto
fun newBundle(configuration: ConfigurationDto): Bundle {
Log.d("some", "new Bundle: configuration $configuration")
return Bundle().apply {
putSerializable("configuration", configuration)
}
}
}
Here I see that the configuration is coming.
And when i trying to get my arguments in fragment:
class OnboardingFragment : Fragment(), ViewPager.OnPageChangeListener, View.OnClickListener {
private fun setupViewModel() {
viewModel = ViewModelProvider(this, viewModelFactory).get(OnboardingViewModel::class.java)
lifecycle.addObserver(viewModel)
Log.d("some", "arguments in vm: $arguments")
viewModel.setInitialData(arguments)
I see that arguments is null.
I see that arguments is null.
arguments is the Bundle that is set on a Fragment, usually when you create it. Something like:
val fragment = OnboardingFrament().apply {
arguments = Bundle().apply {
// Set values on bundle
}
}
Since you have only shown two random pieces of code related to some random ViewModels and not where you create OnBoardingFragment, I can only assume that wherever you create it, you are not setting the arguments, hence it's null.
Your sendData function does nothing. It creates a Bundle object and then immediately releases it to the garbage collector because you aren't doing anything with it. Incidentally, it's weird that it takes a nullable parameter when it can't do anything useful with null. If a function is useless when its parameter(s) is null, it should not accept a nullable parameter. Otherwise, the calling code looks like it's doing something useful when it isn't, which just makes debugging more difficult and bugs more likely.
Also, it is highly unusual that your ViewModels are aware of your Fragments. It is part of the contract of ViewModel that you're not supposed to leak your Activities or Fragments to them. LiveData is special because it can automatically drop its references to its observers at the appropriate time to prevent a leak.
So whatever you're doing by having your ViewModel observe your Fragment lifecycle is probably a design error. You should not be trying to send data to another Fragment by using a ViewModel function.
If you want to send data from one Fragment to another Fragment, then you should set its arguments property right after instantiating the Fragment instance and before making the fragment transaction. Then, as the framework automatically recreates the Fragment as needed for configuration changes, when it automatically creates new instances of that Fragment, it will also pass it that same argument data.
I will take a simple sample.
I have 2 Screens: Screen A and Screen B. From Screen A, I open Screen B. And when I return Screen B to Screen A, I want to transfer data back to Screen A.
With Android Fragment, I can use Shared ViewModel or Fragment Result API to do this.
But with Android Compose, the Fragment Result Api is not in Compose. With using Shard ViewModel, what lifecycle do I have to attach Shared ViewModel so it can keep alive? Activity, ... or something else.
Or is there another way to do this?
If you use jetpack navigation, you can pass back data by adding it to the previous back stack entry's savedStateHandle. (Documentation)
Screen B passes data back:
composable("B") {
ComposableB(
popBackStack = { data ->
// Pass data back to A
navController.previousBackStackEntry
?.savedStateHandle
?.set("key", data)
navController.popBackStack()
}
)
}
Screen A Receives data:
composable("A") { backStackEntry ->
// get data passed back from B
val data: T by backStackEntry
.savedStateHandle
.getLiveData<T>("key")
.observeAsState()
ComposableA(
data = data,
navToB = {
// optional: clear data so LiveData emits
// even if same value is passed again
backStackEntry.savedStateHandle.remove("key")
// navigate ...
}
)
}
Replace "key" with a unique string, T with the type of your data and data with your data.
All of your compose composition operations happens within a single activity view hierarchy thus your ViewModel lifecycle will inevitably be bound to that root activity. It can actually be accessed from your composition through LocalLifecycleOwner.current.
Keep in mind that Compose is a totally different paradigm than activity/fragment, you can indeed share ViewModel across composables but for the sake of keeping those simple you can also just "share" data simply by passing states using mutable values and triggering recomposition.
class MySharedViewModel(...) : ViewModel() {
var sharedState by mutableStateOf<Boolean>(...)
}
#Composable
fun MySharedViewModel(viewModel: MySharedViewModel = viewModel()) {
// guessing you already have your own screen display logic
// This also works with compose-navigator
ComposableA(stateResult = viewModel.sharedState)
ComposableB(onUpdate = { viewModel.sharedState = false })
}
fun ComposableA(stateResult: Boolean) {
....
}
fun ComposableB(onUpdate: () -> Unit) {
Button(onClick = { onUpdate() }) {
Text("Update ComposableA result")
}
}
Here you'll find further documentation on managing states with compose
Let's say there are two screens.
1 - FirstScreen it will receive some data and residing on bottom in back stack user will land here from Second screen by press back button.
2 - SecondScreen it will send/attach some data to be received on previous first screen.
Lets start from second screen sending data, for that you can do something like this:
navController.previousBackStackEntry
?.savedStateHandle
?.set("key", viewModel.getFilterSelection().toString())
navController.popBackStack()
Now lets catch that data on first screen for that you can do some thing like this:
if (navController.currentBackStackEntry!!.savedStateHandle.contains("key")) {
val keyData =
navController.currentBackStackEntry!!.savedStateHandle.get<String>(
"key"
) ?: ""
}
Worked perfectly for me.
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
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 am trying to use the same instance of ViewModel in Parent Fragment and its children, using Navigation Component. The hierarchy is as follows: Single Activity that has navigationHost. This host has 3 child fragments, A, B and C. The last fragment has also navigationHost with 2 fragments: X and Y. The below graph illustrates the hierarchy.
Expected:
I would like to share the same instance of fragment C ViewModel with fragment X and Y.
Current:
The ViewModel of fragment C is initialized twice: Once when fragment C is initialized and second time when fragment X is initialized. The Fragment X is set as a default destination in the fragment C nav graph. When I am changing the default destination to Y, the ViewModel is initialized in C and Y.
What I tried already:
In child viewModels I use this:
val viewModel: ParentViewModel =
ViewModelProvider(findNavController().getViewModelStoreOwner(R.id.parent_graph)).get(
ParentViewModel::class.java
)
In parent viewModel I use this:
val viewModel by viewModels<ParentViewModel>()
I've also tried to inject the viewModel using Koin sharedViewModel with scope of fragment:
val viewModel by sharedViewModel<ParentViewModel>(from = { parentFragment!! })
Also no luck.
Is it possible or maybe it is a bug in navigation library?
A NavHostFragment is a fragment itself, so your structure is actually
Fragment C -> NavHostFragment -> Fragment X
-> Fragment Y
I.e., the parentFragment you get from Fragment X is not Fragment C - it is the NavHostFragment you added in between the two.
Therefore if you want to get a ViewModel from Fragment C, you'd need to use requireParentFragment().requireParentFragment() - the parent of your NavHostFragment is Fragment C.
Cannot find a parameter with this name: from
------------update----------------
for those facing the same issue, check here koin issue discuss about, and maybe here might be helpful.
I'm using
//child fragment
private val viewModel: TripParentViewModel by viewModel(owner = { ViewModelOwner.Companion.from(requireParentFragment().requireParentFragment().viewModelStore)})
//parent fragment
private val parentViewModel by viewModel<TripParentViewModel>()
as solution,
class TripParentViewModel:ViewModel() {
var count = 0
fun test(){
when(count){
0 -> Timber.d("first click")
1 -> Timber.d("second click")
2 -> Timber.d("third click")
}
Timber.d(count.toString())
count++
}
}
currently, I run this when change fragment, I didn't see any problem so far, if anything goes wrong, I will update here
koin_version = "2.2.1"
navigation_version = "2.3.5"