Can we use Android Jetpack Navigation Component with Settings preferences? - android

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.

Related

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.

How to handle navigation properly

I have one question, what should I use to navigate from 1 Activity that hosts multiple fragments.
The goal is 1 Activity that hosts multiple fragments.
I'm using the Navigation Components Architecture
My goal is to know which is the best way to implement the navigation
The currently implemented way of doing navigation is this
class MainMenuActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main_menu)
}
override fun onSupportNavigateUp() = findNavController(R.id.nav_host_fragment).navigateUp()
}
Then to navigate between Fragments after inflating the default one is this (From Fragment A to Fragment B
Fragment A : Fragment() {
onViewCreated(...){
btn.setOnClickListener{
findNavController.navigate(R.id.nextAction)
}
From Fragment B to Fragment C
Fragment B : Fragment() {
onViewCreated(...){
btn.setOnClickListener{
findNavController.navigate(R.id.nextAction)
}
My question is, is it a good practice navigating between fragments this way ? Because I feel like Im doing a navigation between fragments but without caring about the Main container Activity.
What I'm thinking to do is something like this
class MainMenuActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main_menu)
}
override fun onSupportNavigateUp() = findNavController(R.id.nav_host_fragment).navigateUp()
fun navigateToFragment(id:Int){
findNavController.navigate(id)
}
}
And then call this from each fragment to go to a desired destination
Fragment A : Fragment() {
onViewCreated(...){
btn.setOnClickListener{
requireActivity().navigateToFragment(R.id.nextAction)
}
Is this better to have 1 activity that hosts a stack of Fragments, or its better the first way ?
Doing it the first way I think Im hosting fragments within fragments, making me do childFragmentManager to get the fragment manager of those fragments.
And also makes it harder to extend some methods from the activity itself.
Thanks
First of all, you are doing the same thing in both methods. Calling NavigationController from fragment, activity or any other view if that matters will return you the same NavigationController.
Second of all, the point of Navigation Component is to split navigation from its containing Activity. In fact the direct parent of all your fragments are the NavHostFragment that you have defined in your xml. So, activity has nothing to do with navigating between fragments.
Third, regardless of doing "first way" or "second way" (technically they are same thing as I mentioned in my first point) while navigating it does not mean that you are hosting fragments within fragments. Instead Navigation Component will replace your container with new fragment every time you visit new destination.
And finally, it's better to stick with what the developers suggested. Try reading the documentation and you don't see anywhere where they change destination through Activity.
You can use an interface for communicating with the MainActivity from both fragments and do the fragment transaction from MainActivity.

Using NavController with preferences

I'm currently migrating my application to androidX and the new navigation component. My app has different settings screens which are nested into each other. Before androidX the PreferenceScreen were nested into each other. Now days this is handled as:
<Preference
app:fragment="com.example.SyncFragment"
.../>
So in XML we give an absolute class name for the PreferenceFragmentCompat we want to show by clicking on that preference. The fragment transaction has to be implemented by the Activity, which holds the fragments. I would love to do this by my NavControler but here is my problem:
override fun onPreferenceStartFragment(caller: PreferenceFragmentCompat, pref: Preference): Boolean {
val nav = Navigation.findNavController(this, R.id.nav_host_fragment)
nav.navigate(<Fragment ID>) // Only int IDs can be passed here
return true
}
Is there any way I could give the pref reference, which I get through this callback, an ID so I can handle the fragment transaction with the NavControler? Or do I over complicate things here and have missed something?

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)
}
}

Navigation Component .popBackStack() with arguments

I have Two fragment. SecondFragment and ThirdFragment. Actually I use the Navigation Component for passing value between fragments. Like this:
SecondFragment:
val action = SecondFragmentDirections.action_secondFragment_to_thirdFragment().setValue(1)
Navigation.findNavController(it).navigate(action)
Here is how I read the value from the ThirdFragment:
arguments?.let {
val args = ThirdFragmentArgs.fromBundle(it)
thirdTextView.text = args.value.toString()
}
It's work fine. Now my stack is look like this:
ThirdFragment
SecondFragment
There is any option for pass value from the opened ThirdFragment to the previous SecondFragment with the new Navigation Component? (When ThirdFragment is finishing)
I know about onActivityResult, but If Nav.Component serve better solution than I want use that.
Thank you!
It's a bit late for this answer but someone may find it useful. In the updated versions of the navigation component library it is now possible to pass data while navigating back.
Suppose the stack is like this
FragmentA --> FragmentB.
We are currently now in FragmentB and we want to pass data when we go back to FragmentA.
Inside FragmentAwe can create an observer with a key:
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val navController = findNavController()
// Instead of String any types of data can be used
navController.currentBackStackEntry?.savedStateHandle?.getLiveData<String>("key")
?.observe(viewLifecycleOwner) {
}
}
Then inside FragmentB if we change its value by accessing previous back stack entry it will be propagated to FragmentA and observer will be notified.
val navController = findNavController()
navController.previousBackStackEntry?.savedStateHandle?.set("key", "value that needs to be passed")
navController.popBackStack()
Just came across setFragmentResult(), pretty easy to use. The docs on this are here.
If you are navigating: Fragment A -> Fragment B -> Fragment A
Add this to fragment A:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setFragmentResultListener("requestKey") { requestKey, bundle ->
shouldUpdate = bundle.getBoolean("bundleKey")
}
}
Then in fragment B add this line of code:
setFragmentResult("requestKey", bundleOf("bundleKey" to "value to pass back"))
// navigate back toFragment A
When you navigate back to fragment A the listener will trigger and you'll be able to get the data in the bundle out.
What you are asking for is an anti-pattern. You should either
navigate to the second fragment again with the new values you would like to set
use the third fragment ins a separate activity and start it with startActivityForResult()
use a ViewModel or some kind of singleton pattern to hold on to your data (make sure you clear the data after you no longer need it)
these are some of the patterns that came to my mind. Hope it helps.
As described here:
When navigating using an action, you can optionally pop additional destinations off of the back stack. For example, if your app has an initial login flow, once a user has logged in, you should pop all of the login-related destinations off of the back stack so that the Back button doesn't take users back into the login flow.
To pop destinations when navigating from one destination to another, add an app:popUpTo attribute to the associated element. app:popUpTo tells the Navigation library to pop some destinations off of the back stack as part of the call to navigate(). The attribute value is the ID of the most recent destination that should remain on the stack.
<fragment
android:id="#+id/c"
android:name="com.example.myapplication.C"
android:label="fragment_c"
tools:layout="#layout/fragment_c">
<action
android:id="#+id/action_c_to_a"
app:destination="#id/a"
app:popUpTo="#+id/a"
app:popUpToInclusive="true"/>
</fragment>

Categories

Resources