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.
Related
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.
Long time ago the normal and suggested way on how to communicate between fragments were to create a interface that was extended in Activity. Basically, if I wanted to get data from Fragment B to Fragment C, I had to do Fragment B to MainActivity and then MainActivity to Fragment C. In my view - total mess.
Just started to use LiveData and thought of using it instead of callbacks - for instance:
FragmentInteraction.kt
val onOkayButtonClicked = SingleLiveEvent<Void>()
val onCancelButtonClicked = SingleLiveEvent<Void>()
FragmentA.kt
onOkayButtonClicked.call()
FragmentB.kt
onOkayButtonClicked.observe(viewLifecycleOwner, Observer {
// do whatever u want
})
Does this approach has any down-sides? Is there an easier / more elegant way to approach this?
You could have only one ViewModel(lifecycleOwner = activity), that would have all the liveDataEvents you need for the interactions. You will act on the liveData from the onClick events:
okayButton.setOnClickListener { viewModel.onOkayButtonClicked() }
This LiveData will be observed by the NavigationComponent, or similar you are using for handling fragment transactions, and it will do the transitions for you.
All these transitions I recommend doing them on the Activity level.
Is it possible to use Android Jetpack Navigation Component to navigate the fragments of a Settings Preferences? I have a Settings screens with multiple nested Preference Fragments. I was wondering if it is feasible to use Android Jetpack Navigation Component to facilitate my task?
The google documentation states that hierarchical nesting of preferences are now deprecated in favour of inter-fragment navigation; that is the structure <PreferencesScreen> ... <PreferencesScreen ...> ... </PreferenceScreen> ... </PreferenceScreen> is now out.
The documents propose that one replace the nested <PreferenceScreen/> with a preference and set the app:fragment attribute, hence
<xml ...>
<PreferencesScreen ...>
<Preference
android:title="Title"
app:fragment="TLD.DOMAIN.PACKAGE.preferences.SUBPreferenceFragment"
app:destination="#+id/SUBPreferenceFragment" # Not supported by Schema
android:summary="Summary"/>
</PreferenceScreen>
Note : It'd be mighty handy if they allowed app:destination="#+id/SUBPreferenceFragment" to identify the navigation destination; I simply duplicate app:destination from the navigation graph's XML file.
From the documentation it is then possible to navigate between the PREFERENCEFramgents by referencing this preference.fragment as follows
class Activity : AppCompatActivity(),
// Preferences Interface
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback
{
...
override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat, preference: Preference): Boolean
{
// Instantiate the new Fragment
val args = preference.extras
val fragment = supportFragmentManager.fragmentFactory.instantiate(
classLoader,
preference.fragment)
fragment.arguments = args
fragment.setTargetFragment(caller, 0)
// Replace the existing Fragment with the new Fragment
supportFragmentManager.beginTransaction()
.replace(R.id.FRAGMENT, fragment)
.addToBackStack(null)
.commit()
return true
}
...
}
There is however a small Snafoo ! R.id.FRAGMENT in a normal activity represents the placeholder within the activities' view where one would swap the main fragments in an out of existence. When using Navigation R.id.FRAGMENT maps to R.id.NAVIGATION and the code above swaps out the entire navigation fragment with the preference. For an activity using Navigation a reformulation is necessary.
class Activity : AppCompatActivity(),
// Preferences Interface
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback
{
...
override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat, preference: Preference): Boolean {
// Retrieve the navigation controller
val navctrl = findNavController(R.id.navigation)
// Identify the Navigation Destination
val navDestination = navctrl.graph.find { target -> preference.fragment.endsWith(target.label?:"") }
// Navigate to the desired destination
navDestination?.let { target -> navctrl.navigate(target.id) }
}
...
}
While this works I am a bit dubious about the line that determines the navDestination. If you have a better incantation for val navDestination = navctrl.graph.find { target -> preference.fragment.endsWith(target.label?:"") } then please edit the question or leave a comment accordingly so I can update the answer.
Yes, you can use Navigation components inside Preference fragments.
I have also a main preference fragment with multiple nested fragments. For me the best way was to navigate just inside preference fragments without any activity. So I created onPreferenceClickListeners and navigate just like you would from a normal fragment.
findPreference(getString(R.string.PREF_GN_BUTTON_MAIN)).setOnPreferenceClickListener(preference5 -> {
Navigation.findNavController(requireView()).navigate(R.id.another_settings_fragment);
return true;
});
The advantage of this way is that you can use gradle plugin Safe args to navigate between destinations. So it is a little bit safer than to use it only with ids just like Carel from Activity.
I tried to override onPreferenceStartFragment in my activity but I could not see any advantages of it. I use single Activity architecture.
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
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)
}
}