How to save fragment state while using Navigation component and Bottom Navigation? - android

I have four fragment in bottom navigation and one activity, Bottom navigation is set up using NavController like this
navController = Navigation.findNavController(this, R.id.dashboardNavHostFragment).apply {
setGraph(R.navigation.nav_graph , bundle)
}
bottomNavigationView.setupWithNavController(navController)
I'm passing some data in bundle that I require in my first fragment of bottom nav.
The problem is it works fine the data comes in the fragment like this
private val args: PlayerFeedFragmentArgs by navArgs()
private var data: String? = args.name
but when I navigate using bottom nav and come back to my first fragment the data comes as null
I tried saving data using
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString("name", data)
}
and getting data back in onViewCreated but it didn't work because onSaveInstanceState does not get called when switching between fragments of bottom nav.
How can I save the incoming args from the activity in my first fragment so that when I switch fragments from bottom nav it stays the same.

You can try using a SavedStateViewModel handler
https://developer.android.com/reference/kotlin/androidx/lifecycle/SavedStateViewModelFactory
A quick example can be found here:
Using this method, you don't have to share your data with other fragments.

Related

Problem with android toolbar using navigation drawer

List the item
I'm developing an app using a navigation drawer and navigation components and I'm facing two issues:
I settled specifically each toolbar title where it is supposed to be, but every time I change the fragments, in the toolbar, for an instant, I can see the previous name from the fragment, which is the fragment name itself. So, it quickly changes from MySpecificFragment to MyFragmentName and I would like it to not happen. I've settled the title even onCreateView or onViewCreated. It didn't matter, still happening.
How could I decide the direction in which the back button of the fragment goes? I would like to create a standard position where the back button arrow goes, always the same. But it just travels back to the previous fragment (which is not a real problem, but I would like to improve its behavior)
Sorry for the lack of code, I don't know what I am supposed to display since I'm going against the standard android behavior.
P.S.: Using android studio and kotlin
Regarding the first issue, one way to avoid the brief display of the previous fragment name in the toolbar is to set the toolbar title in the parent activity and then update it from the fragment's onResume() method. This ensures that the toolbar title is set correctly when the fragment is resumed after being pushed onto the back stack. Here's an example code snippet:
In your activity:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
}
fun setToolbarTitle(title: String) {
supportActionBar?.title = title
}
}
In your fragment:
class MySpecificFragment : Fragment() {
override fun onResume() {
super.onResume()
(activity as? MainActivity)?.setToolbarTitle("MySpecificFragment")
}
}
Regarding the second issue, you can customize the back button behavior by using a custom NavController.OnDestinationChangedListener. In the listener, you can set the back button icon and its behavior based on the current and previous destinations. Here's an example code snippet:
class MyNavigationController(activity: AppCompatActivity, navController: NavController) {
init {
navController.addOnDestinationChangedListener(
activity, object : NavController.OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
if (destination.id == R.id.my_fragment) {
activity.supportActionBar?.setDisplayHomeAsUpEnabled(false)
} else {
activity.supportActionBar?.setDisplayHomeAsUpEnabled(true)
activity.supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_arrow_back)
activity.supportActionBar?.setHomeActionContentDescription(R.string.back)
activity.supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_arrow_back)
activity.supportActionBar?.setDisplayShowHomeEnabled(true)
}
}
})
}
}
Here, you can adjust the back button icon and behavior based on the current and previous destinations by setting setDisplayHomeAsUpEnabled, setHomeAsUpIndicator, and setHomeActionContentDescription.

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

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.

Navigation Component Lifecycle

I have been trying out Navigation Component for a while now but somehow not getting the reason (or explanation) behind current Lifecycle of Navigation Component. Here are some points that needs clarification.
1. Fragment to Fragment flow
In navigation Component every fragment (or lets say page) is recreated every time it is visited (or revisited). So, when you are at A and go to B, A is destroyed and later when you go back to A (like pressing back button) A is created from stretch.
In a traditional Activity patterns when you go back to A it just goes to onResume as it wasn't destroyed when moving to B. Any reason that this pattern is changed in Navigation Component?
The problem of recreating is when you have a lot of data and it takes time to get redrawn and it feels like app is freezing. An example would be simple HomePage (say Facebook NewsFeed). Preserving data can be handled with ViewModel but drawing of all of the feeds again require time and it will freeze.
There is another problem that recreation generates. Assume this scenario: A has an Collapsing AppBar with a NestedScrollView. User scrolls down and AppBar will collapse and then user moves to a different page B. When he comes back to A it will be redrawn and AppBar is expanded. I am not sure if it is a bug or I should do something to fix it? So any solution here is also welcomed.
2. Activity recreation
If activity is recreated for certain reasons like orientation change, every page in Navigation Component gets recreated until current destination. Although onCreate method of Fragment not onCreateView is getting called, I don't see a point of creating Fragments in Activity recreation. Any explanation would be welcomed.
Navigation component only supports fragment replacement as of now. So you won't be able to add() a fragment as you do it with Manual fragment transaction.
However, if your worry is about re-inflating the layout and re-fetching the data for the fragment, it could be easily resolved with below two methods.
Once the view is created, store it in a variable and use it whenever onCreateView() is called.
private var view: View? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
if (view == null) {
view = inflater.inflate(R.layout.fragment_list, container, false)
//...
}
return view
}
Source: https://twitter.com/ianhlake/status/1103522856535638016
Use ViewModel with the Fragment and hold the data required as a member variable. By this way, the data is not cleared when you replace the associated fragment. The ViewModel gets cleared only on onDestroy() of the fragment, which will only happen when you destroy the parent activity. https://developer.android.com/images/topic/libraries/architecture/viewmodel-lifecycle.png
The way that we use fragments to bridge data and views has changed slightly, and in a good way, when migrating to the Navigation library. It forces us to distinguish between Fragment and View lifecycles.
Pre-navigation: observe LiveData in onCreate() using Fragment's lifecycleScope.
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
...
import kotlinx.android.synthetic.main.old_fragment.*
class OldFragment : Fragment(R.layout.old_fragment) {
private val vm by activityViewModels<MainViewModel>()
override fun onCreate(savedInstanceState: Bundle?) {
vm.getLiveData().observe(this) { data ->
oldTextView.text = data.name
}
}
}
Navigation: observe LiveData in onViewCreated() using viewLifecycleOwner scope.
...
class NewFragment : Fragment() {
private val vm by activityViewModels<MainViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
vm.getLiveData().observe(viewLifecycleOwner) { data ->
oldTextView.text = data.name
}
}
}
Key Notes:
Not all Lifecycle Owners are the same. The fragment lifecycle will not execute the observer when the view is recreated (while using Navigation library and navigating up/back).
The viewLifecycleOwner cannot be accessed before the view is created.
Hopefully, this can help prevent replacement of LiveData code as developers migrate to Navigation.

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