Android: Fragment restore with BottomNavigationView, NavController and SafeArgs - android

i'm currently working on an Android app and encountered a problem concerning BottomNavigationView and Fragments. I know, there are similar questions like mine but either they doesn't solve my problem or they have no working answers.
My app consists of five top-level destination fragments. For navigating between them I use the BottomNavigationView. Additionally, I have several fragments which serve as lower-level destinations and will be called from one of the top-level fragments. I use SafeArgs plugin to navigate to these fragments and also to pass data to.
My BottomNavigationView Configuration looks like this:
val navView: BottomNavigationView = findViewById(R.id.nav_view)
val navController = (supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment).navController
val appBarConfiguration = AppBarConfiguration(setOf(
R.id.navigation_dest1, R.id.navigation_dest2, R.id.navigation_dest3,
R.id.navigation_dest4, R.id.navigation_dest5))
setupActionBarWithNavController(navController, appBarConfiguration)
navView.setupWithNavController(navController);
The problem of this usage is that BottomNavigationView doesn't seem to provide support for saving and storing the fragments somewhere and reuse these instances for navigation. It just creates a new instance and displays it.
Currently each fragment contains some data fetching code, e.g. running a network request in a coroutine or loading files from the filesystem. And because BottomNavigationView doesn't preserve fragment instances, these data fetching parts are run too often.
Of course I thought about putting the data fetching process into the main activity but this results in an overall slower app-startup and doesn't solve the problem that the fragments still need to be recreated every time the user navigates between them.
Up to this point, I already found half of a solution. By using the SupportFragmentManager, manually adding, showing and hiding my fragments it works. But the app runs noticeably slower and the navigation to my lower-level destinations with SafeArgs just doesn't work anymore. I use SafeArgs because it's easy to use and pretty hassle-free, and I would like to keep using it.
I tried to manage it all manually with SupportFragmentManager, but it ends up in chaos and worse performance.
Is there any known way my problem can be solved? A way, BottomNavigationView can interact with SafeArgs and SupportFragmentManager to reuse the fragments instead of recreating them on each navigation action?
(If you need further information or parts of my code, please ask. I think posting my complete code here doesn't make much sense.)

Have you considered the option of sharing a ViewModel with your fragments ? For example:
Create a ViewModel class like the following:
class MyViewModel: ViewModel() {
....
....
}
Then, because your fragments share the same Activity, you can declare the following (in Kotlin):
class MyFragment1: Fragment() {
val viewModel: MyViewModel by activityViewModels()
....
....
}
class MyFragment2: Fragment() {
val viewModel: MyViewModel by activityViewModels()
....
....
}
In this case Fragment1 and Fragment2 will share the same ViewModel instance and the ViewModel will remain in memory until the activity is destroyed.
Fragments won't be preserved when you navigate out, but you can preserve all data of each fragment and re-use them. It is fast and smooth and you won't mind if the fragment is re-created because all its data will be kept in memory and ready for use in the shared ViewModel.
See also the official documentation:
ViewModel Overview

Related

Set ViewModel in one fragment, and fetch the new data in other fragments in SingleActivity app with NavHost?

I have some trouble with a SingleActivity with a NavHost app in android. There is some text strings from EditText widgets in a fragment that I need to put into a ViewModel, and update the other fragments with the new ViewModel data. I do this from MainActivity in an OnClick method from a floating action button that I is outside the NavHost implementation.
So, I've tried
FragmentManager fm = getSupportFragmentManager();
CreateFragment fragment = (CreateFragment) fm.findFragmentById(R.id.fragment_create);
fragment.saveInputFromTextFields();
But here findFragmentById returns null, and I don't know why.
I have a solution from a question I asked previously, that I use in MainActivity to find the current displayed fragment and calling a method from it;
Fragment navHostFragment = getSupportFragmentManager().findFragmentById(R.id.nav_host_fragment);
if (navHostFragment instanceof NavHostFragment) {
Fragment visibleFragment = navHostFragment.getChildFragmentManager().getFragments().get(0);
if (visibleFragment instanceof HomeFragment) {
((HomeFragment) visibleFragment).saveInputFromTextFields();
}
}
But this doesn't work after one use, because navHostFragment.getChildFragmentManager().getFragments().get(0); always returns the previous fragment, and changing get(0) to get(1) crashes the app with a IndexOutOfBoundsException.
Maybe I'm going about this the wrong way.
What I really need is a reliable way of setting ViewModel data in one fragment and updating the other fragments with the new data.
I've heard of something called LiveData or DataBinding, do I need to implement something like this? It feels like overkill.
The data I'm working with is one array list in the ViewModel that contains items that fill a recycler view. I use the fragments for creating new items or editing current items, that I just need to get access to somehow. I feel like there is a simpler way, but I don't know how to implement this. Please tell me if you need more information from me.
Thank you for your time.
The Navigation Components are intended to be used primarily for navigating between top level destinations. Therefore a NavHostFragment has a single destination that is currently active at any one time which explains why you can only retrieve the current fragment from its fragment manager.
If you're looking to share data between fragments I would looking into using a ViewModel scoped to your activity so that both your fragments can hold reference. There is some starter documentation here: Share data between fragments. There is also a codelab which will give you a head start into shared view models but also working with LiveData which I would definitely recommend: Shared ViewModel Across Fragments Codelab.
I realized I was going about it the wrong way. The data I needed to update can be updated when the fragments are destroyed and created. So there's really no need for me to tell an instance of the fragment to update the data manually, as that can be handled by android.
Thank you all for your replies.

How to create a shared variable between fragments in kotlin

currently, my Kotlin based application consists of a single activity, 3 fragments and a navigation (with navigation drawer) between them.
How do I add a variable, which will be initialized in the start of the application, will be visible in all fragments, and can be updated in one of them?
a simple int or string for that matter so it should be with little overhead as possible, yet i'd like to follow correct coding conventions. Please elaborate on the correct function to perform the initial variable value, how to bind each fragment textview to it, and the correct way to set the new value.
Thanks!
Create ViewModel and fragments use viewmodel like this
val viewModel: YourViewModel by activityViewModels()
in that case your viewmodel's scope is the same as activity scope.
For more information please refer to this link

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.

How to use a shared ViewModel, and avoid reusing the same instance of it every time with Navigation Component

My app consists of one single Activity with multiple Fragments, following the "single Activity app model", so that I can implement properly navigation using the Navigation Component in Android jetpack.
Most of my screens (Fragments) are standalone and don't depend on each other, hence they use their own ViewModel
Some features require navigation involving more than one Fragment. As those features share data between them that are passed back and forth through the Fragments, I use a shared ViewModel (as recommended by Google).
I need to use the same instance of the shared ViewModel in all the associated Fragments, as I need the Fragments share the state of the shared ViewModel.
To use the same instance of the ViewModel in these associated Fragments, I need to create the ViewModel using the parent Activity (not the Fragment) when getting the ViewModel from the ViewModelProviders:
val viewModel = ViewModelProviders.of(
parentActivity, factory.create()
).get(SharedViewModel::class.java)
This works, however, It produces one problem:
when navigating a consecutive times to the first Fragment that requires the shared ViewModel, ViewModelProviders.of() will return the same instance of the ViewModel as before: The ViewModel is being shared between the Fragments, but also between different navigations to the feature implemented like this.
I understand why this is happening (Android is storing the ViewModel in a map, which is being used when requesting the ViewModel with ViewModelProviders.of()), but I don't know how I am expected to implement the "shared ViewModel pattern" properly.
The only workarounds I see are:
Create a different Activity for the feature that uses the Fragment with a shared ViewModel
Use nested Fragments, and use common parent Fragment for the feature that uses the Fragment with a shared ViewModel
With these two options, I would be able to create a ViewModel that will be shared between the Fragments intervening in the feature, and will be different each time I navigate to the feature.
The problem I see here is that this seems to be that against the fundamentals of the Navigation Component and the single Activity app.
Each feature implemented this way will need to have a different navigation graph, as they will use a different navigation host. This would prevent me from using some of the nice features of Navigation Component.
What is the proper way to implement what I want?
Am I missing anything, or is it the way it is?
Before Navigation Component I would use different Activities and Fragments and use Dagger scopes associated with the Activity/Fragment to achieve this. But I'm not sure what's the best way of implementing this with just one Activity`
I have discovered this can be done starting with 2.1.0-alpha02
From:
https://developer.android.com/jetpack/androidx/releases/navigation#2.1.0-alpha02
You can now create ViewModels that are scoped at a navigation graph
level via the by navGraphViewModels() property delegate for Kotlin
users or by using the getViewModelStore() API added to NavController.
b/111614463
Basically:
in the nav graph editor, create a nested graph, assigning one id to it
when providing the ViewModel, do not do it from the Activity. Instead, use
the navGraphViewModels extension function of Fragment.
Example:
Nested graph in the nav graph
<navigation
android:id="#+id/feature_nested_graph"
android:label="Feature"
app:startDestination="#id/firstFragment">
<argument
android:name="item_id"
app:argType="integer" />
<fragment
android:id="#+id/firstFragment"
[....]
</fragment>
[....]
</navigation>
For getting the ViewModel scoped to feature_nested_graph nested nav gaph:
val viewModel: SharedViewModel
by fragment.navGraphViewModels(R.id.feature_nested_graph)
or, if are injecting into the ViewModel and you are using a custom factory for that:
val viewModel: SharedViewModel
by fragment.navGraphViewModels(R.id.feature_nested_graph) { factory2.create(assessmentId) }
You have the same instance of a shared ViewModel, because it belongs to the Activity - that's clear. I don't know exactly your use case, but usually when I need to do similar, I simply notify the ViewModel from Fragment's onCreate or orCreateView passing some identifier. In your case it could be something like:
viewModel.onNavigatedTo("fragment1")
This way the shared view model can differentiate what fragment currently uses it and refresh the state accordingly.

injecting viewmodel with navigation-graph scope: NavController is not available before onCreate()

I'm using a navigation-component in my application and also using shared ViewModel between multiple fragments that are in the same graph. Now I want to instantiate the ViewModel with this graph scope with this.
As you know, in fragments we should inject objects ( ViewModel,..etc ) in onAttach:
but when I want to do this (injecting ViewModel with a graph scope in onAttach), this error occurs:
IllegalStateException: NavController is not available before onCreate()
Do you know how I can do this?
In short, you could provide the ViewModel lazily with dagger Provider or Lazy.
The long explanation is:
Your injections points are correct. According to https://dagger.dev/android#when-to-inject
DaggerActivity calls AndroidInjection.inject() immediately in
onCreate(), before calling super.onCreate(), and DaggerFragment does
the same in onAttach().
The problem is some kind of race condition between when Android recreates the Activity and the Fragments attached to the FragmentManger and when the NavController can be provided. More specifically:
one Activity that has Fragments attached is destroyed by the OS (can be reproduced with "don't keep Activities" from "developer settings")
user navigates back to the Activity, OS proceeds to recreate the Activity
Activity calls setContentView while being recreated.
This causes the Fragments in the FragmentManager to be reattached, which involve calling Fragment#onAttach
The Fragment is injected in Fragment#onAttach
Dagger tries to provide the NavController
BUT you cannot get the NavController from the Activity by this point, as Activity#onCreate has not finished yet and you get
IllegalStateException: NavController is not available before onCreate()
The solution I found is to inject provide the NavCotroller or things that depend on the NavController (such as the ViewModel, because Android needs the NavController to get nav-scoped VideModels) lazily. This can be done in two ways:
with Lazy
with Provided
(REF: https://proandroiddev.com/dagger-2-part-three-new-possibilities-3daff12f7ebf)
ie: inject the ViewModel to the Fragment or implementation of navigator like this:
#Inject
lateinit var viewModel: Provider<ViewModel>
then use it like this:
viewModel.get().events.observe(this) {....}
Now, the ViewModel can by provided by Dagger like:
#Provides
fun provideViewModel(
fragment: Fragment,
argumentId: Int
): CreateMyViewModel {
val viewModel: CreateMyViewModel
by fragment.navGraphViewModels(R.id.nested_graph_id)
return viewModel
}
Dagger won't try to resolve the provisioning when the Fragment is injected, but when it's used, hence, the race condition will be solved.
I really hate not being able to use my viewModels directly and need to use Provider, but it's the only workaround I see to solve this issue, which I'm sure it was an oversight by Google (I don't blame them, as keeping track of the absurd lifecycle of Fragment and Activities is so difficult).
...we should inject objects ( ViewModel,..etc ) in onAttach...
Looks like it is currently a no go for such injection with the original by navGraphViewModels(R.id.nav_graph) delegated property provided by androidx.navigation package because from the source code
findNavController().getBackStackEntry(navGraphId) and
public final NavController getNavController() it stated that:
* Returns the {#link NavController navigation controller} for this navigation host.
* This method will return null until this host fragment's {#link #onCreate(Bundle)}
And here are some workarounds:
https://github.com/InsertKoinIO/koin/issues/442

Categories

Resources