Android ViewModel Sharing Between Fragments [duplicate] - android

I'm using the navigation component, I want a view model to be shared between a few fragments but they should be cleared when I leave the fragments (hence not scoping them to the activity) I'm trying to take the one activity many fragments approach. I have managed to achieve this using multiple nav hosts and scoping the fragments to it using getParentFragment but this just leads to more issues having to wrap fragments in other parent fragments, losing the back button working seamlessly and other hacks to get something to work that should be quite simple. Does anyone have a good idea on how to achieve this? I wondered if theres anything with getViewModelStore I could be using, given the image below I want to scope a view model to createCardFragment2 and use it in anything after it (addPredictions, editImageFragment, and others i haven't added yet), but then if I navigate back to mainFragment I want to clear the view models.
BTW I cant just call clear on mainFragment view model store as there are other view models here that shouldn't be cleared, I guess i want a way to tell the nav host what the parent fragment should be which I'm aware isn't going to be a thing, or a way to make the view model new if I'm navigating from mainFragment or cardPreviewFragment

Here's a concrete example of Alex H's accepted answer.
In your build.gradle (app)
dependencies {
def nav_version = "2.1.0"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
}
Example of view model
class MyViewModel : ViewModel() {
val name: MutableLiveData<String> = MutableLiveData()
}
In your FirstFlowFragment.kt define
val myViewModel: MyViewModel by navGraphViewModels(R.id.your_nested_nav_id)
myViewModel.name.value = "Cool Name"
And in your SecondFlowFragment.kt define
val myViewModel: MyViewModel by navGraphViewModels(R.id.your_nested_nav_id)
val name = myViewModel.name.value.orEmpty()
Log.d("tag", "welcome $name!")
Now the ViewModel is scoped in this nested fragment, shared state will be destroyed when nested nav is destroyed as well, no need to manually reset them.

Yes, it's possible to scope a viewmodel to a navgraph now starting with androidx.navigation:*:2.1.0-alpha02. See the release notes here and an example of the API here. All you need to give is the R.id for your navgraph. I find it a bit annoying to use, though, because normally viewmodels are initialized in onCreate, which isn't possible with this scope because the nav controller isn't guaranteed to be set by your nav host fragment yet (I'm finding this is the case with configuration changes).
Also, if you don't want your mainFragment to be part of that scope, I would suggest taking it out and maybe using a nested nav graph.

so when i posted this the functionality was there but didn't quite work as expected, since then i now use this all the time and this question keeps getting more attention so thought i would post an up to date example,
using
//Navigation
implementation "androidx.navigation:navigation-fragment:2.2.0-rc04"
// Navigation UI
implementation "androidx.navigation:navigation-ui:2.2.0-rc04"
i get the view model store owner like this
private ViewModelStoreOwner getStoreOwner() {
NavController navController = Navigation
.findNavController(requireActivity(), R.id.root_navigator_fragment);
return navController.getViewModelStoreOwner(R.id.root_navigator);
}
im using the one activity multiple fragments implementation, but using this i can effectively tie my view models to just the scoped fragments and with the new live data you can even limit that too
the first id comes from the nav graphs fragment
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<fragment
android:id="#+id/root_navigator_fragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="androidx.navigation.fragment.NavHostFragment"
app:defaultNavHost="true"
app:navGraph="#navigation/root_navigator"/>
</FrameLayout>
and the second comes from the id of the nav graph
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/root_navigator"
app:startDestination="#id/mainNavFragment">
and then you can use it like so
private void setUpSearchViewModel() {
searchViewModel = new ViewModelProvider(getStoreOwner()).get(SearchViewModel.class);
}

So based on the answers here I made a function that lazily returns a ViewModel scoped to the current navigation graph.
private val scopedViewModel by lazy { getNavScopedViewModel(arg) }
/**
* The [navGraphViewModels] function is not entirely lazy, as we need to pass the graph id
* immediately, but we cannot call [findNavController] to get the graph id from, before the
* Fragment's [onCreate] has been called. That's why we wrap the call in a function and call it lazily.
*/
fun getNavScopedViewModel(arg: SomeArg): ScopedViewModel {
// The id of the parent graph. If you're currently in a destination within this graph
// it will always return the same id
val parentGraphScopeId = findNavController().currentDestination?.parent?.id
?: throw IllegalStateException("Navigation controller should already be initialized.")
val viewModel by navGraphViewModels<ScopedViewModel>(parentGraphScopeId) {
ScopedViewModelFactory(args)
}
return viewModel
}
It's not the prettiest implementation but it gets the job done

Related

One-Single Activity or Two Activities

My app's main Activity (containing a NavigationDrawer) allows to navigate through many (20 aprox) Fragments, because of navDrawer item clicks and other views' clicks inside each fragment.
Then, it moves to a point where I need a BottomNavigationView (maintaining also the navDrawer). From this point, because of the bottomNavView and other views' clicks, I can move to other different 10-15 fragments, aprox, and also to the ones that the main NavigationDrawer allows, but, in case I move to a Fragment through a click on any main navDrawer's item, the bottomNavView should be hidden.
So, is it correct here to use a One-Single Activity approach and be controlling the visibility of the bottomNavView or shall I use Two Activities in order to avoid being pendent of this in all navigations?
I don't believe there's a "right or wrong" answer in this case.
It really boils down to how you want to architect your application, as long as you're consistent.
If your fragments have a "state" and a ViewMOdel and such, then a single activity swapping fragments while controlling its own state (when to show the bottom bar) may be simpler than having to maintain two different activities, since navigation is done always between fragments.
It will also be tied to how the backstack behaves in each case (so test accordingly to ensure you get the expected behavior).
Simple Idea (with a single act)
This is pseudo-code, not perfect, compiling, functional code.
class BottomBarUseCase() {
operator fun invoke(destination: String): Boolean =
when (destination) {
"A", "B", "C" -> true
else -> false
}
}
Your Activity's ViewModel (greatly simplified of course)
class XXXViewModel(
private val bottomBarUseCase: BottomBarUseCase
): ViewModel() {
private val _state = MutableLiveData<YourState>(YourState.Empty)
fun setupBottomBar(destination: String) {
if (bottomBarUseCase(destination)) {
_state.value = SomeState.ShowBar
} else {
_state.value = SomeState.HideBar
}
}
Your Activity observes the state and does what it needs to do.
There are ways to streamline this and what not, but essentially, you're delegating the responsibility to show the bar to the use-Case, which you can test in isolation to ensure it does what you want it to do (aka: don't show the bar for certain destinations).
Your Fragments don't care about any of this (unless they too, need to make the decision, in which case you can still inject the useCase in the Fragment's viewModels and ask there too, since the useCase doesn't have any special dependency).
That's what I would do, but without having to do this in real life, it's hard to visualize whether this would have other drawbacks.
In general, this is how I would approach a problem that needs to be resolved elsewhere in many places: isolating it.
Hope that clarifies it.

RecyclerView with StaggeredGridLayoutManager in ViewPager, arranges items automatically when going back to fragment

I am using Navigation component in my App, using google Advanced Sample(here).
my problem is when going back to a fragment, the scrolling position does not lost but it rearranges items and moves highest visible items so that top of those item align to top of recyclerview. please see this:
before going to next fragment:
and after back to fragment:
this problem is matter because some times clicked item goes down and not seen until scroll down.
how to prevent this behavior?
please consider:
this problem exist if using navigation component to change fragment. if start fragment using supportFragmentManager.beginTransaction() or start another activity and then go to this fragment it is OK. but if I navigate to another fragment using navigation component this problem is exist.(maybe because of recreating fragment)
also this problem exist if using fragment in ViewPager. i.e recyclerView is in a fragment that handle with ViewPagerAdapter and viewPager is in HomeFragment that opened with Navigation component. if recyclerView is in HomeFragment there is no problem.
no problem with LinearLayoutManager. only with StaggeredGridLayoutManager.
there is not difference if using ViewPager2 and also FragmentStatePagerAdapter
I try to prevent recreate of fragment(by this solution) but not solved.
UPDATE:
you can clone project with this problem from here
When using Navigation Component + ViewPager + StaggeredGridLayoutManager, wrong recyclerView.computeVerticalScrollOffset() has been returned during Fragment recreate.
In general, all layout managers bundled in the support library already know how to save and restore scroll position, but in this case, we had to take responsibility for this.
class TestFragment : Fragment(R.layout.fragment_test) {
private val testListAdapter: TestListAdapter by lazy {
TestListAdapter()
}
private var layoutManagerState: Parcelable? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
postListView.apply {
layoutManager = StaggeredGridLayoutManager(
2, StaggeredGridLayoutManager.VERTICAL
).apply {
gapStrategy = StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
}
setHasFixedSize(true)
adapter = testListAdapter
}
testListAdapter.stateRestorationPolicy = RecyclerView.Adapter.StateRestorationPolicy.PREVENT
}
override fun onPause() {
saveLayoutManagerState()
super.onPause()
}
override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
restoreLayoutManagerState()
}
private fun restoreLayoutManagerState () {
layoutManagerState?.let { postListView.layoutManager?.onRestoreInstanceState(it) }
}
private fun saveLayoutManagerState () {
layoutManagerState = postListView.layoutManager?.onSaveInstanceState()
}
}
Source code: https://github.com/dautovicharis/MyStaggeredListSample/tree/q_65539771
The Navigation Component behavior is normal when you navigate from one fragment to another. I mean, onDestroyView() method from the previous fragment is executed, so it means that your view is destroyed, but not the fragment. Remembering that fragment has two lifecycles one for the fragment and another one for the view, There was a video about it.
Also, there were issues registered in issue tracker in order to avoid this behavior in some cases and the GitHub issues:
https://issuetracker.google.com/issues/127932815
https://github.com/android/architecture-components-samples/issues/530
The problem is that when you have fragment that is heavy to recreate, is easier to do not destroy it and just add one fragment. So, when you go back it is not recreated. But, for this behavior is not part of navigation component.
Solutions
The easiest solution is to not use navigation component and work with the tradicional way, as you can see this works perfectly in you use case.
You can use the traditional way just for this use case, and use the navigation component for other cases.
You can inflate this view in an activity. So you are adding un activity
But if the previous tree options is not possible. You can try the following:
If you are using viewModel, you can use SaveState. Basically, it can save the data from your fragment, it is like a map data structure, so you can save positions from your list or recycler view. When go back to this fragment, get the position from this saveState object and use scrollToPosition method in order to add the real position.
Recycler view have methods for restore positions. You can see the uses cases for that, because first you need the data and then add the real position, for more details you can visit this link. This configuration for recycler view is useful also when you lose memory and you need to recreate the recycler view with asynchronous data.
Finally, if you want to understand more about how fragment works with navigation component, you can see this link

Android navigation component view decision

I am using Android Navigation Component in my project. What I am not satisfied is the fragment making the decision to do fragment transition to next fragment transition i.e
In my LoginFragment I have this -
viewModel.onLoginPressed(email, password)
.observe(viewLifecycleOwner, Observer {
if (it.userLoggedIn) {
activity?.findNavController(R.id.nav_host_fragment)
?.navigate(R.id.action_loginFragment_to_productsFragment)
}
})
According to me, the view must be dummy and must not do any such decisions of what to do on loginSuccess for example. The viewModel must be responsible for this.
How do I use this navigation component inside viewModel?
The ViewModel doesn't need to know about navigation, but it knows about events and state.
The communication should be more like:
NavHostActivity -> Fragment -> ViewModel
Your Fragment has Views, and click listeners, and state. The user types a user/password and presses a button. This onClick listener will tell the view model -> (pseudo-code) onUserPressedTheLoginButtonWith(username, password)
The ViewModel in turn will receive this, do what it needs (like check if you're already logged or whatever, perhaps the final decision is to navigate to another fragment).
What the ViewModel will do is expose a LiveData like
val navigationEvent = LiveData<...>
So the viewModel will navigationEvent.postValue(...)
The Fragment should observe this viewModel.navigationEvent.observe(...) { }
And in THERE in the fragment it can either navigate directly or -if you have an Interface- use it like:
yourNavigator.navigateTo(...) //either your VM knows the destination or the yourNavitagor has a navigateToLogin() concrete method, this is all "it depends what you like/prefer/etc.".
In summary
Activity Host contains Nav code, irrelevant.
Fragment(s) can communicate to a NavDelegate created by you (and likely injected) or Fragments simply know the details and do it by themselves.
Fragment(s) observe the navigation state/event from the viewModel.
Fragment(s) push events from the UI (clicks, actions, etc.) to this viewModel
The ViewModel decides what to do and updates the "liveData"(s) (there can be more than one type of thing you want to observe, not only navigation).
The Fragment(s) react to this observation and act accordingly, either doing it themselves or delegating (step 2 ^).
That's how I'd do it.

Android Navigation Dialog Fragment callback

I have DialogFragment in my project(MVVM, Jetpack navigation) that called from different places and represents signature canvas. Related part in navigation:
<dialog
android:id="#+id/signPadDialogFragment"
android:name="com.ui.signpad.SignPadDialogFragment"
android:label="SignPadDialogFragment" />
<fragment
android:id="#+id/loginFragment"
android:name="com.ui.login.LoginFragment"
android:label="#string/login_label"
tools:layout="#layout/login_fragment">
<action
android:id="#+id/action_loginFragment_to_currentJobsFragment"
app:destination="#id/currentJobsFragment" />
<action
android:id="#+id/action_loginFragment_to_signPadDialogFragment"
app:destination="#id/signPadDialogFragment" />
<fragment
android:id="#+id/jobDetailFragment"
android:name="com.ui.jobdetails.JobDetailFragment"
android:label="job_detail_fragment"
tools:layout="#layout/job_detail_fragment" >
<action
android:id="#+id/action_jobDetailFragment_to_signPadDialogFragment"
app:destination="#id/signPadDialogFragment" />
</fragment>
and navigate action:
mainActivityViewModel.repository.navigationCommands.observe(this, Observer { navEvent ->
navEvent.getContentIfNotHandled()?.let {
navController.navigate(it as NavDirections)
}
})
So, my question is: what is the right way to handle callbacks using Jetpack navigation and MVVM?
I see two possible solution and related questions:
I can pass data to ViewModel -> Repository from dialog fragment( and in this case: how to differ action that started dialog inside dialog scope?)
Or get a callback in MainActivity(How?)
Thanks in advance
Due to the limitations of NavController API, it can only be discovered from element that has android context present. That means your prime options are:
AndroidViewModel: I would not recommend it as it is very easy to get carried away with context here, which will lead to memory leaks if you don't know what you are doing.
Activity: Handle the navigation in the Activity. Single Activity architecture would complicate matters because you'd have to cramp all the navigation logic here.
Fragment: Handle the navigation for each fragment in its scope. This is way better but there is an even better solution below.
ViewModel: Handle each fragment's navigation in a viewModel scoped to it. (Personal preference).
Using ViewModel in Jetpack Navigation Component
Technically navigation login will still reside in Fragment, there is no escaping that unless navigation API changes, however we can delegate major part to the ViewModel as follow:
ViewModel will expose a SingleLiveEvent encapsulating a NavDirection in it. SingleLiveEvent is a Live Data that only gets triggered once, which is what we want when it comes to navigation. There is a great blogpost by Jose Alcérreca on that:
https://medium.com/androiddevelopers/livedata-with-snackbar-navigation-and-other-events-the-singleliveevent-case-ac2622673150
Fragment will observe this SingleLiveEvent and will use that NavDirection to carry out a navigation transaction.
ViewModel exposing SingleLiveEvent:
open class BaseViewModel : ViewModel() {
/**
* Navigation Component API allows working with NavController in the Following:
* 1.) Fragment
* 2.) Activity
* 3.) View
*
* In order to delegate the navigation logic to a viewModel and allow fragment
* or an activity to communicate with viewModel, we expose navigationCommands
* as LiveData that contains Event<NavigationCommand> value.
*
* Event<T> is simply a wrapper class that will only expose T if it has not
* already been accessed with the help of a Boolean flag.
*
* NavigationCommand is a Sealed class which creates a navigation hierarchy
* where child classes can take NavDirections as properties. We will observe the
* value of NavigationCommand in the fragment and pull the NavDirections there.
*/
private val _navigationCommands = MutableLiveData<Event<NavigationCommand>>()
val navigationCommands: LiveData<Event<NavigationCommand>>
get() = _navigationCommands
fun navigate(directions: NavDirections) {
_navigationCommands.postValue(Event(NavigationCommand.To(directions)))
}
}
Fragment observing this SingleLiveEvent from ViewModel:
private fun setupNavigation() {
viewModel.navigationCommands.observe(viewLifecycleOwner, Observer {
val navigationCommand = it.getContentIfNotHandled()
when (navigationCommand) {
is NavigationCommand.To -> { findNavController().navigate(navigationCommand.directions) }
}
})
}
You can make use of BaseFragment and BaseViewModels to follow DRY, but always remember that anything that has Base as a prefix will turn into a code smell soon, so keep them as concise as possible.

Shared ViewModel to help communication between fragments and parent activity

While Navigation component of JetPack looks pretty promising I got to a place where I could not find a way to implement something I wanted.
Let's take a look at a sample app screen:
The app has one main activity, a top toolbar, a bottom toolbar with fab attached.
There are 2 challenges that I am facing and I want to make them the right way.
1. I need to implement fragment transactions in order to allow replacing the fragment on the screen, based on the user interaction.
There are three ways I can think of and have this implemented:
the callbacks way. Having a interface onFragmentAction callback in fragment and have activity implement it. So basically when user presses a button in FragmentA I can call onFragmentAction with params so the activity will trigger and start for example transaction to replace it with FragmentB
implement Navigation component from JetPack. While I've tried it and seems pretty straightforward, I had a problem by not being able to retrieve the current fragment.
Use a shared ViewModel between fragment and activity, update it from the fragment and observe it in the activity. This would be a "replacement" of the callbacks
2. Since the FAB is in the parent activity, when pressed, I need to be able to interact with the current visible fragment and do an action. For instance, add a new item in a recyclerview inside the fragment. So basically a way to communicate between the activity and fragment
There are two ways I can think of how to make this
If not using Navigation then I can use findFragmentById and retrieve the current fragment and run a public method to trigger the action.
Using a shared 'ViewMode' between fragment and activity, update it from activity and observe it in the fragment.
So, as you can see, the recommended way to do navigation would be to use the new 'Navigation' architecture component, however, at the moment it lacks a way to retrieve the current fragment instance so I don't know how to communicate between the activity and fragment.
This could be achieved with shared ViewModel but here I have a missing piece: I understand that fragment to fragment communication can be made with a shared ViewModel. I think that this makes sense when the fragments have something in common for this, like a Master/Detail scenarion and sharing the same viewmodel is very useful.
But, then talking between activity and ALL fragments, how could a shared ViewModel be used? Each fragment needs its own complex ViewModel. Could it be a GeneralViewModel which gets instantiated in the activity and in all fragments, together with the regular fragment viewmodel, so have 2 viewmodels in each fragment.
Being able to talk between fragments and activity with a viewmodel will make the finding of active fragment unneeded as the viewmodel will provide the needed mechanism and also would allow to use Navigation component.
Any information is gladly received.
Later edit. Here is some sample code based on the comment bellow. Is this a solution for my question? Can this handle both changes between fragments and parent activity and it's on the recommended side.
private class GlobalViewModel ():ViewModel(){
var eventFromActivity:MutableLiveData<Event>
var eventFromFragment:MutableLiveData<Event>
fun setEventFromActivity(event:Event){
eventFromActivity.value = event
}
fun setEventFromFragment(event:Event){
eventFromFragment.value = event
}
}
Then in my activity
class HomeActivity: AppCompatActivity(){
onCreate{
viewModel = ViewModelProviders.of(this, factory)
.get(GlobalViewModel::class.java)
viewModel.eventsFromFragment.observe(){
//based on the Event values, could update toolbar title, could start
// new fragment, could show a dialog or snackbar
....
}
//when need to update the fragment do
viewModel.setEventFromActivity(event)
}
}
Then in all fragments have something like this
class FragmentA:Fragment(){
onViewCreated(){
viewModel = ViewModelProviders.of(this, factory)
.get(GlobalViewModel::class.java)
viewModel.eventsFromActivity.observe(){
// based on Event value, trigger a fun from the fragment
....
}
viewModelFragment = ViewModelProviders.of(this, factory)
.get(FragmentAViewModel::class.java)
viewModelFragment.some.observe(){
....
}
//when need to update the activity do
viewModel.setEventFromFragment(event)
}
}

Categories

Resources