How to find the current graphId of android navigation component - android

I am using Android navigation component and I want to create a viewModel with the current graph scope.
I need to use navGraphViewModels(graphId).
the problem is that I use current fragment inside different graphs. how should I get graphId runtime based on the graph?

From what I know, there is no official method of doing that.
None of the options that I've thought about are super good, but you could try a few different approaches:
You could store this id globally, or pass it through all the fragments. Not beautiful but this guy made it work: https://stackoverflow.com/a/60819820/3005235
You could probably use the way navGraphViewModels works and hack your way to find the id.
"This property can be accessed only after this NavGraph is on the NavController back stack, and an attempt access prior to that will result in an IllegalArgumentException."
Using this information, you could try having a list of the all possible different graphs ids, try/catch the viewModel assignment using navGraphViewModels with each one of them and you will find the current one when the exception is not thrown. Not nice but maybe it could work.
But I would say that the limitation of not being able to get the current graphId says something more about how Navigation works and that maybe you are looking for something else... navGraphViewModels(graphId) works by checking the NavController backstack, if there is a NavBackStackEntry referenced by the graphId and that is a instance of NavGraph, it uses that NavBackStackEntry as the ViewModelStoreOwner that is needed to the navGraphViewModels internals (https://androidx.tech/artifacts/navigation/navigation-fragment-ktx/2.2.0-source/androidx/navigation/NavGraphViewModelLazy.kt.html). The thing about not being able to get the current graphId is because it is possible to have more than one NavGraph currently in the backstack (if you navigate from one graph to another), so what is the current one? Is it the one closer to the top of the backstack?
I've tried thinking about your problem as needing to create a viewModel with the current backstack scope, something like changing
val viewModel by navGraphViewModels(graphId)
to
val viewModel by viewModels(navBackStackEntry)
and pass the first navBackStackEntry, to have the outermost scope, but maybe it is not what you want.
viewModels() delegate simply needs a ViewModelStoreOwner (which every NavBackStackEntry is one)
And as I've said, navGraphViewModels(graphId) works by finding the navBackStackEntry associated to this graphId and using it as the ViewModelStoreOwner, you would be doing a similar thing but to create a specific NavBackStackEntry scoped viewModel and not a 'current' graph scoped. You probably want the last entry from the NavBackStack, to make it work as scoping by the graphId would? This way is probably a little bit complicated but you would have more control over the scope that you want to use.
I've also thought of checking all the NavBackStackEntries and finding one that is related to a NavGraph and you could use that with by viewModels and achieve almost the same effect as navGraphViewModels but sadly it is not possible:
I hope this investigation can help you, somehow. After all that, try/catching all possible graphIds may be a valid workaround.

Related

Keep ViewModel instance when navigating back with by navGraphViewModel

So I am following Google's approach where you use single activity for the whole app and multiple view models, which are scoped to your nested nav graphs or fragments.
And I have a problem with this approach. Let's use example provided by google docs so it would be easier for me.
Here when user reached destination C, the back stack contains one instance of each destination(A, B, C) and if we want to return to destination A without multiplying instances of our destinations inside back stack - we need to add popUpTo="#+id/a" and popUpToInclusive="true" to clear all instances inside the back stack.
Here is the problem - if we will use this then every old instance of destinations will be cleared and new instance of A will be created hence if we are using by navGraphViewModel(R.id.A) then our old instance of ViewModel will be cleared as well!
So how to avoid such behavior? Is there a way to open destination A without creating a new instance so that our view model won't be cleared as well?
I have tried multiple ways to prevent that, but nothing seemed to work
(in this case A - could be a nested graph also and that would work the same way)
Edit: I Just read your question again and I think I misunderstood it the first time. My answer might not be of much help.
Is your requirement to have sub (nested) NavGraphs? If not, I don't think navGraphViewModels() applies to you. From what I read, viewmodels created using this approach are cleared when their corresponding NavGraph is completed.
navGraphViewModels is very useful when we are creating fragment for some modules and we don’t want these viewModels to be persist throughout the whole activity. Hence, we can create our own custom sub-navGraph with navGraphViewModels() .
That is, if you simply want to achieve a single activity / multiple fragments architecture, why not use by activityViewModels() inside each fragment. Using this, the viewmodel instance is persisted as long as the host activity is alive. Thus allowing you to share data between the activity and all the fragments.
Reference: https://skynight1996.medium.com/navigation-component-comparison-between-viewmodels-activityviewmodels-and-ae0145734228

How to share ViewModel scope between a particular set of Fragments, without using NavGraphs or scoping it to the activity?

Google states to use SharedViewModel in case of communication between Fragments by scoping it to the activity.
In a Single Activity Application, this means that the activity will be littered with ViewModels which might not be needed anymore and they will stay there for the whole lifecycle. Considering some example like a extended sign up flow or something considering a few screens.
One way recommended was using a parent Fragment as scope, but that is only possible if there is a parent fragment. It can be just different Fragments.
I came up with the following solution, I want to know how viable or how bad is it and is there any better way around?
Considering I have two Fragments called ExampleOneFragment and ExampleTwoFragment for sake of simplicity and I want them to have a shared scope without actually scoping it to the activity. Let's say I want to update text view in ExampleOneFragment from ExampleTwoFragment so I create a SharedViewModel like this for both
For ExampleOneFragment it will be:
private val mSharedViewModel by lazy {
ViewModelProvider(this).get(SharedViewModel::class.java)
}
And For ExampleTwoFragment I came up with this:
private val mSharedViewModel by lazy {
ViewModelProvider(supportFragmentManager().findFragmentByTag(ExampleOneFragment.TAG) ?: this).get(SharedViewModel::class.java)
}
This seems to be working, but I don't know what kind of issues can this cause.
Some Other Solutions are which I found:
According to #mikhehc here We could actually create our very own ViewModelStore instead. This will allow us granular control to what scope the ViewModel have to exist. But I don't understand how to make it work for Fragments?
Secondly, is the hacky way of scoping it to the activity still, but clearing it out via dummy viewmodel by using same key which I found here
Could anyone guide me what is the right approach? I cannot shift to NavGraphs since it is an already up and running project and scoping to activity just feels wrong. Thanks.
This seems to be working, but I don't know what kind of issues can this cause.
This code will only work if:
An ExampleOneFragment is created first, always
It is added to the same FragmentManager that ExampleTwoFragment uses, via a tag of ExampleOneFragment.TAG, always
If either of those assumptions fail, you will wind up with separate viewmodel instances, because supportFragmentManager().findFragmentByTag(ExampleOneFragment.TAG) ?: this will resolve to this.
But I don't understand how to make it work for Fragments?
You would use it the way it is shown in that answer, or by anything else that accepts a ViewModelStoreOwner. In this line:
val viewModel = ViewModelProvider(myApp, viewModelFactory).get(CustomViewModel::class.java)
You would get myApp from your Fragment as:
val myApp = requireContext().application as MyApp
Personally, I think the solution that you pointed to is very risky. The ViewModelStore lives for the entire lifetime of the process and is never cleared. You will wind up sharing your viewmodel across everything, and everything done by that viewmodel is leaked. The concept of creating a custom ViewModelStoreOwner is fine, but you should be doing something to tie the scope of that owner to the relevant lifetime for the viewmodels that it stores. The answer tries to dance around that in its last paragraph; too many developers will ignore this and run into problems.
One way recommended was using a parent Fragment as scope, but that is only possible if there is a parent fragment. It can be just different Fragments.
Your app is not written in a way to make automatic ViewModelStoreOwner management available "out of the box", then.
In the end, you are looking for a ViewModelStoreOwner to be shared between these two fragments. In your first solution, you are trying to hack that by using the ViewModelStoreOwner from one of those fragments, which only works if you can reliably choose which fragment that is. In the solution you pointed to in that other answer, you are trying to hack that by intentionally leaking a ViewModelStoreOwner.
There may be other approaches that you could consider, depending on your circumstances. For example, there may be some option with your dependency inversion framework (Dagger/Hilt, Koin, etc.) to rig up a ViewModelStoreOwner that is tied to a specific pair of fragment instances.

How parentFragmentViewModel could communicate with subFragmentViewModel?

While discovering the MVVM with Kotlin and Android, I'm facing a small problem related to the organization of one of my fragments.
Suppose I have an activity that hosts a fragment and after a navigation (with NavController) the activity host a new fragment, which has multiples subfragments (perhaps through a ViewPager). All of the 3 fragments (the parent & the 2 children) must display precise part of a data. Furthermore the second subfragment has a button that could change the data & this change must update the UI of all the fragments.
Firstly in my mind, I was thinking all the data will be stored inside the parentFragmentViewModel due to the fact that their will be useful for the 3 fragments, but that's where my problem appeared.
How the subfragments's viewModels could handle these data & update it?
My first thought seems to be incorrect, because if we read the viewModel doc, we can see "However ViewModel objects must never observe changes to lifecycle-aware observables, such as LiveData objects."
So, my subFragments's ViewModels can't observe the parent one. I was thinking about sharing the same viewModel between the 3 fragments but I don't know if it's a bad practice or not and I don't know how to do it the cleanest way possible.
How can I resolve my problem?
EDIT
After further research, I tried this solution https://stackoverflow.com/a/53819347/7861724
I created the viewModel inside the parentfragment. Once done, I get it inside my subfragment.
It currently work but I'm not sure if it's a good practice.
Why do you need to observe any lifecycle-aware component? You can create setters for MutableLiveData in your ViewModel if you need to update it from he newly created fragments, this doesn't mean that the ViewModel is observing changes.
val data: MutableLiveData<String>
fun updateData(val newData: String) {
data.value = newData
}
The fragments can actually listen to changes from the ViewModel, but that's fine because in the moment they are destroyed, the observers will also stop observing. That means that you can have your buttons and everything updated with no memory leaks.

Android send ViewModel to Fragment via NavDirections arguement (Safe-Args)

I have an app with the following architecture:
Navigator is a custom class that holds the NavController
Cooridnator holds the Navigator
Cooridnator tells the Navigator to "start" the framgent and passes the ViewModel to it
Navigator asks NavController to navigateTo a NavDirections and provides the required arguments (using Safe-Args)
Now the issue here is that if I want to send the ViewModel as argument, it needs to be Parcelable and all of its underlying classes as well (which would make most of my code Parcelable, and that's not really needed).
So is there a way to do this without making everything Parcelable or using Dagger ? (Don't like Dagger as it adds too much complexity to the code...)
I would be okay with having a lateinit field in the Fragment and setting it manually but can't seem to access the Fragment from NavDirections
Any idea on how I could do this ?
First of all: what you are passing in safe args is "data" while your viewmodel is logic. Which means your data can change over the time (one of examples would be to become outdated) but as long as viewmodel is unchanged, it's logic would stay. Thus passing viewmodel itself does not make sense to me - best you can is to pass its snapshot of state, but I doubt that's what you want.
So yes, you should be using DI and there are alternatives to dagger complexity. You can experiment with koin (because I see kotlin in your tags list), some basic outline of what it can is here https://shorturl.at/bflFL (medium). You can also experiment with Hilt as what appears to be simplified alternative to Dagger, for android world.

ViewModel for Fragment instead accessing Activity ViewModel?

The problem is quite straightforward. The question is in context of using ViewModels, LiveData and other related Lifecycle aware arch approaches.
I have an Activity with NavDrawer, which switches fragments inside. And also I have a case when two fragments are present at the same time on the screen - this will be the main pain.
One Fragment has a ViewPager with nested Fragments(don't ask why).
The other fragment is just obtaining info from first one when user performs some actions. This is achieved just by sharing activity viewmodel. But the app itself has a lot of business logic and as it goes further the viewmodel goes bigger and bigger.
What I want to ask - not a receipt or rules how to fix this, or maybe how to overcome this by fixing the entire structure of the project. I want to ask for suggestions how can I apply the MVVM approach within android.arch.lifecycle style to mine use-case.
I haven't seen something more complicated then just sharing the Activity ViewModel between Fragments. But common, that's not a cure.
What you can see here - a mess actually. The point is that all are sharing the ActivityViewModel. Connections(aggregation) from FirstFragment mean that ViewPager inside FirstFragment is initiating ChildFragments and they are also working with the same ActivityViewModel(kill me). So as result everyone is working with one shared ViewModel.
My proposal is to add a ViewModel for each Layer. So that Activity/Fragments/ChildFragments have their own ViewModels.
But what appears here - how we should communicate then?
Possible solutions :
Having two ViewModels per one component. One ViewModel will handle/delegate the business logic and another will make the communication. Two viewmodels per component - not so good, yeah?
Having old manner interface(please no!)
Other workarounds - like DB/SharedPrefs/Realm change listeners and Event Buses(I'm too old for this :( ).
Your solution here!
I'll say that all of the above are breaking a lot of design principles, so what should I do?
How should I come out of this mess? Is there any Uncle Bob or another superhero here to help?
P.S. - Well, creating UMLs or other charts isn't mine forte. Sorry for that.
P.P.S. - I'm aware of google samples.
What i would suggest you can do is handle two ViewModel for your entire use case.
Make one ViewModel
Let's say MyActivityViewModel to handle all logic related for activity level. So, if any fragment logic is directly related to your activity then share your ViewModel like below :
ViewModelProviders.of(getActivity()).get(MyActivityViewModel.class); // Like this in fragment.
&
ViewModelProviders.of(this).get(MyActivityViewModel.class); // Like this in activity.
This will share common ViewModel between your activity and fragment.
Another ViewModel would go for FirstFragment in your case if you have to share logic between your ChildFragment :
Here you can share ViewModel let's say FragmentViewModel like below:
ViewModelProviders.of(this).get(FragmentViewModel.class); // Like this in FirstFragment which is having view pager.
&
ViewModelProviders.of(getParentFragment()).get(FragmentViewModel.class); // Like this in View pager fragments, getParentFragment() is First fragment in our case.
Although, we can still use our activity level MyActivityViewModel in our child fragments from FirstFragment like :
ViewModelProviders.of(getActivity()).get(MyActivityViewModel.class);
First there is no harm in having multiple ViewModel's for a single View.
I would think about my ViewModel's like what kind of data is getting and manipulating, and group them in a way, that seems natural.
For your case, if the fragments and the activity's logic is very similar, I think you can go with a single ViewModel, but I would avoid that.
What I would do is break the activity's ViewModel into smaller parts and reuse the proper ViewModel's in my Fragments, so that I wouldn't have a God ViewModel, nor roughly the same code in different ViewModel's.
This is updated version of answer given by Jeel Vankhede. And also Kotlin implementation of the same.
Since ViewModelProviders is deprecated now we have to use ViewModelProvider.
Here is how you do it in Activity:
ViewModelProvider(this).get(MyActivityViewModel::class.java)
Here is how you do in Fragment:
ViewModelProvider(requireActivity()).get(MyActivityViewModel::class.java)
To solve the problem of FirstFragment sharing its view model with its child fragments, you can use this code to access the FirstFragmentViewModel from any of the child fragments:
// in ChildFragment1
val firstFragmentViewModel: FirstFragmentViewModel by viewModels(
{ requireParentFragment() }
)

Categories

Resources