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

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.

Related

How to find the current graphId of android navigation component

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.

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.

Is it okay to access activity's properties and functions from fragment?

I'm new to android, I wanted to know if it is okay to access properties initialized in activity / call activity functions from fragment like this or is it bad practice and I should avoid it.
(requireContext() as BaseActivity).viewModel
(requireContext() as BaseActivity).countryList
(requireContext() as BaseActivity).getSomething()
Your instincts can be right. Breaking changes can be caused by name conflicts, variable shadowing, wrong imports, wrong assignment to values. But these days, the demand for features is increasing, in such that you need the public accessor. Just have this rules in your conscience:
Interfaces are powerful at class to class communication
Inherit what is important, override what is implemented already, pass to param to lessen global var damage
If a variable can be stored in another global form, consider it with regard to size(ram matters), speed of access(ux matters), security(keys matter) and volatility(nulls matter).
Now looking at your code, I can see you have a fragment system that is based on values/functions stored in the main activity, that provides the context for the fragment. If you apply the first point: Your fragment will implement a BaseFragment that already some context cast i.e. lateinit var mainActivity: MainActivity then you can mainActivity.viewModel anywhere in your fragment without casting. And this is cleaner
Applying the second point: in the BaseFragment (that will be inherited by AnotherFragment)
abstract var viewModel: ViewModel
abstract fun initList()
open var countryList = mutableListOf()
open fun onScale(detector: ScaleGestureDetector) { //pinch: increase visible country list like some nice zoom effect .. etc }
if most or all of your fragments need similar functions or variables, make abstract to something you can forget will crash the app, make open for those 'features but I dont need to rewrite so I'll call super.function' functions. Make a var open if some super function overrides it, and just put var if you seriously dont know when you want it and when to change it.
On the third point, Android in the early stages, we learnt the hard way that context doesn't last forever even if your app is running. Rotation and lifecycle functions will swap it rough and fast. So consider other storage ways. I still dont trust requireContext/Activity/view for context, so cast with caution.
A big NO. It is a bad practice to use hardcode references to activity from fragments.
I see that you are using viewmodel, which indicates that you are using MVVM, you should use Sharing data between Activities and Fragments in MVVM made simple with SharedViewModel concept for communicating to viewmodel of the activity.
For communicating to the Activity which hosts your Fragment you should use an interface pattern of communication. from fragment to the activity
Let's say one of your activity won't extend the base activity as it has to extend one activity from a library lets say YouTubeBaseActivity, and it will host a particular fragment now the cast to BaseActivity will never succeed in your activity.

Activity scoped regular class that acts as a ViewModel

In the Using Dagger in your Android app codelab tutorial they use an activity scoped regular class that acts as a ViewModel like so
#ActivityScope
class RegistrationViewModel #Inject constructor(val userManager: UserManager) {
...
}
That makes ViewModel injection by Dagger very simple but won't we loose anyting if we don't derive from the architecture components ViewModel class?
In general, the code labs are related to some topic and they try to explain only this topic. Here it is Dagger, not Architecture Components. Yes, you can lose some functionality, but if they still can make their point - it does not matter.
Also if they make the app work with only plain java objects it means that they don't need the extra functionality from the ViewModel, they wrote less code so even better.
I also want to point out that the explanation that "you are losing ViewModel.onCleared" is "the smallest problem". What is the "main feature" of the VM is that you can share the same instance through the life cycle of the same Activity/Fragment or that you can share it between different Activity/Fragment.
And onCleared is something that should be used with caution because it means in some situations that you are trying to clear a reference to a thing you should not be holding in the first place.

Should we Use ViewModels to communicate between 2 different fragments or bundles in single activity App Architecture?

Scenario 1 - If we use ViewModels to communicate between fragments, then the ViewModel has to be created by activity reference and hence going to stay there in memory until the activity is destroyed.
Scenario 2 - In master-detail flow ViewModel makes our life easier but again the memory usage issue is there.
Scenario 3 - We have viewModelScope in the new version of arch library to cancel jobs with Fragment/Activity lifecycles, but if ViewModel is created with activity reference then it's going to stay there until activity is destroyed. Hence the job can still be executing and fragment is already gone.
You can use ViewModels to communication between two different fragments(aka SharedViewmodels) because it's simple but it's not perfect.
As you know the SharedViewModels must be alive until the first joint parent (activity or fragment) is alive.
but wait ...
What is the purpose of ViewModels?
Are we create ViewModels just for communication between fragments? Absolutely not.
Is the use of ViewModels against the purpose of ViewModels? no, I say it's not perfect use them for communication between fragments but if you have a small project you can use them because it's simple.
So what can I do instead of using ViewModels for communication between fragments? You can build independent UI components and use them for communication between fragments.
What is the advantage of building this weird independent UI components? In this way, each component has its own ViewModel (or Presenter) and they haven't any parent/child relation; Instead, they updated from the same business logic or in reactive programming they are just observing the same Model.
What is the meaning of building independent UI components and how to build them? if you are already familiar with reactive programming, I recommend reading this amazing blog post by Hannes Dorfmann; If you don't, I simply propose using EventBus library for communication between fragments but You will soon realize that too much usage of this library leads to spaghetti code.
Scenarios
If the fragments are not part of a flow/group, then don't share the ViewModel, just pass some id/data to the new fragment, create its own viewmodel, and query the data for the fragment from its own viewmodel.
If the fragments are part of some flow/group (cart/checkout/booking flow, multi-screen registration process, viewpager fragments, etc) and the logic is common enough, then share the viewmodels between the fragments. In single-activity architecture, I put these flow/process in its own root parent fragment that acts as a host and is used to create the scope of the viewmodel. For example:
MainActivity ->
-> RootAuthFragment
-> SplashFragment (replace with below)
-> LoginFragment (add to backstack with below or onsuccess login go to MainFragment)
-> SignupFragment (onsuccess go to Main)
-> MainFragment (replace with RootAuthFragment)
In the above scenario, you can share the viewmodel between login and signup screens with RootAuthFragment's scope. If you have a multi-screen signup process, then you could move that into separate root fragment and create a separate viewmodel for the signup flow.
Bundle vs ViewModels:
Bundles are used to pass some values. So, use it just for that. I use bundles to usually pass primitive data types or enums and based on that I query the actual data from the viewmodel (through android room or retrofit) or if the data objects are small enough, I make them parcelable and just pass that.
If you have a shared ViewModel and it's becoming a god class and does a lot of different things, then it means those fragments need separate ViewModels. Don't share the ViewModel just for data. Share the ViewModel for the common/shared behaviour/data/logic (whichever makes sense for your particular use cases)
I prefer you should use View Models approach if you are using single activity architecture. To justify my answer I will clear your scenarios here.
Scenario 1 - If we use ViewModels to communicate between fragments, then the ViewModel has to be created by activity reference and hence going to stay there in memory until the activity is destroyed.
Scenario 2 - In master-detail flow ViewModel makes our life easier but again the memory usage issue is there.
As for memory you are already holding information into memory there is no escaping there. If you don't need data for stay there then you can clear data from models also but again it will kill the purpose of storing data in the first place.
If you pass data using bundle it's also going to take memory there also.
Scenario 3 - We have viewModelScope in the new version of arch library to cancel jobs with Fragment/Activity lifecycles, but if ViewModel is created with activity reference then it's going to stay there until activity is destroyed. Hence the job can still be executing and fragment is already gone.
That's the main purpose of using view models it will store the last state for user where he left.
As per https://developer.android.com/topic/libraries/architecture/viewmodel states
This approach offers the following benefits:
The activity does not need to do anything, or know anything about this communication.
Fragments don't need to know about each other besides the SharedViewModel contract. If one of the fragments disappears, the other one keeps working as usual.
Each fragment has its own lifecycle, and is not affected by the lifecycle of the other one. If one fragment replaces the other one, the UI continues to work without any problems.

Categories

Resources