Fragments not added to backstack using Navigation Components - android

Information:
I'm programmatically inserting a NavHostFragment for each feature of the app. Each NavHostFragment has it's own Navigation Graph. Dagger is providing them by using a FragmentFactory specific to each feature. It's a Single Activity structure with MVVM architecture.
Repo: https://github.com/mitchtabian/DaggerMultiFeature/tree/nav-component-backstack-bug
checkout the branch "nav-component-backstack-bug".
The Problem
When navigating into the graph the fragments are not being added to the backstack. The only fragment that's added is whichever one has most recently been visited. So the stack size always stays at one.
Originally I thought it was because I wasn't setting the FragmentFactory to the ChildFragmentManager but that doesn't change anything. See the code snippets below for the relevant code. Or checkout the project and run it. I have logs printing out the fragments currently in the backstack from the ChildFragmentManager and also the SupportFragmentManager. Both have a constant size of 1.
Feature1NavHostFragment.kt
This is one of the custom NavHostFragment's. The create() function in the companion object is how I create them.
class Feature1NavHostFragment
#Inject
constructor(
private val feature1FragmentFactory: Feature1FragmentFactory
): NavHostFragment(){
override fun onAttach(context: Context) {
((activity?.application) as BaseApplication)
.getAppComponent()
.feature1Component()
.create()
.inject(this)
childFragmentManager.fragmentFactory = feature1FragmentFactory
super.onAttach(context)
}
companion object{
const val KEY_GRAPH_ID = "android-support-nav:fragment:graphId"
#JvmStatic
fun create(
feature1FragmentFactory: Feature1FragmentFactory,
#NavigationRes graphId: Int = 0
): Feature1NavHostFragment{
var bundle: Bundle? = null
if(graphId != 0){
bundle = Bundle()
bundle.putInt(KEY_GRAPH_ID, graphId)
}
val result = Feature1NavHostFragment(feature1FragmentFactory)
if(bundle != null){
result.arguments = bundle
}
return result
}
}
}
MainActivity.kt
This is an example of how I create the NavHostFragment's in MainActivity.
val newNavHostFragment = Feature1NavHostFragment.create(
getFeature1FragmentFactory(),
graphId
)
supportFragmentManager.beginTransaction()
.replace(
R.id.main_nav_host_container,
newNavHostFragment,
getString(R.string.NavHostFragmentTag)
)
.setPrimaryNavigationFragment(newNavHostFragment)
.commit()
Feature1MainFragment.kt
And here is an example of how I'm navigating to other fragments in the graph.
btn_go_next.setOnClickListener {
findNavController().navigate(R.id.action_feature1MainFragment_to_feature1NextFragment)
}
Summary
Like I said, in every fragment I'm printing the backstack for both the ChildFragmentManager and the SupportFragmentManager. Both have a constant size of one. It's as if the fragments are being replaced as I navigate into the graph instead of added to the stack.
Anyways, thanks for reading this and I would appreciate any insights.

Looks like a misunderstanding on my part (and a bug, also on my part).
If you loop through the fragments in the childFragmentManager it only ever shows the top-most fragment for some reason.
Example
val navHostFragment = supportFragmentManager
.findFragmentByTag(getString(R.string.NavHostFragmentTag)) as NavHostFragment
val fragments = navHostFragment.childFragmentManager.fragments
for(fragment in fragments){
// Only prints a single fragment, no matter the backstack size
}
However, if you print the backstack size like this, you will get the correct answer.
val navHostFragment = supportFragmentManager
.findFragmentByTag(getString(R.string.NavHostFragmentTag)) as NavHostFragment
val backstackCount = navHostFragment.childFragmentManager.backStackEntryCount
println("backstack count: $backstackCount")
At the end of the day this misunderstanding caused me to believe the fragments were not being added to the backstack. All is good.

Related

How to change the defaultNavHost among a few already created fragments?

I have a couple of fragments in my activity, each having their own Navigation Graph, they're created when the activity is created. What I want to know is how can I choose any of them to be the defaultNavHost or the PrimaryNavigationFragment after they are created?
The code below creates them and sets them as the default one, but I don't want to recreate them each time. How can I do that?
val finalHost = NavHostFragment.create(navigation)
supportFragmentManager.beginTransaction()
.replace(R.id.navFragment0, finalHost)
.setPrimaryNavigationFragment(finalHost) // this is the equivalent to app:defaultNavHost="true"
.commit()
Couldn't find the code to do what I wanted, so here's my workaround:
First remove the #setPrimaryNavigationFragment(finalHost) from fragmentManager's transaction in your Code, and/or remove app:defaultNavHost="true" from the <fragment...> in your XML.
Then create your NavHostFragments and add each of them to a HashMap:
private val navHostFragments = HashMap<Int, NavHostFragment?>()
fun createNavHosts() {
val finalHost: NavHostFragment =
NavHostFragment.create(navigationId)
.also { navHostFragments[aHandleToTheNavHost] = it }
}
Finally in onBackPressed get the navController of the proper navHost and navigate up on it:
override fun onBackPressed() {
if (navHostFragments[aHandleToTheNavHost]?.navController?.navigateUp() == false)
super.onBackPressed()
}
Note: when #navigateUp() returns false, it means there was nowhere to navigate up to. So in there you can finish the activity or anything else you may want to do.

Navigation's back stack is lost when navigation is nested

I have a navigation which looks like this
Frag1 -> Frag2 -> Frag3
Inside Frag2 there is a NavHostFragment with its own navigation
InnerFrag1 -> InnerFrag2
If I do this
Navigate to Frag2
Navigate to InnerFrag2 inside Frag2
Navigate to Frag3
Go back
then I'll see InnerFrag2 inside Frag2, when I press back normally I would go from InnerFrag2 to InnerFrag1 inside Frag2 but now it's going to Frag1 instead.
Here is my navigation handling inside Frag2
private val backPressedCallback = OnBackPressedCallback {
navHostFragment.navController.navigateUp()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
requireActivity().addOnBackPressedCallback(backPressedCallback)
}
override fun onDestroyView() {
activity?.removeOnBackPressedCallback(backPressedCallback)
super.onDestroyView()
}
private val navHostFragment: NavHostFragment
get() = childFragmentManager.findFragmentById(R.id.innerNavHostFragment) as NavHostFragment
When going back to Frag2 the fragment in the nav host is the correct one, but navigating back moves away from Frag2 because inner nav host's back stack is lost. Can I persist it somehow or fix it some other way?
EDIT: actually when going from Frag3 to Frag2 I see InnerFrag1 inside, the both look alike, that's why going back at this point brings me back to Frag1
EDIT2: I found my problem, I inflate Frag2s navigation from code in onViewCreated like this
val navHostFragment = (frag2NavHostFragment as? NavHostFragment) ?: return
val inflater = navHostFragment.navController.navInflater
val graph = inflater.inflate(navigationId)
navHostFragment.navController.graph = graph
setting it in xml makes it work, I still need to set it from code somehow, Frag2 chooses which navigation to use depending on its arguments
Now my question changes from Navigation's back stack is lost to How to preserve NavHostFragment's state when settings it's graph from code
You can now handle onBackPress on fragments. In your fragment just add this in onViewCreated method.
val navController = Navigation.findNavController(view)
requireActivity().onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
navController.popBackStack(R.id.fragmentWhereYouWantToGo, false)
}
})
I would also give a check to app:popUpTo , app:popUpToInclusive or singleTop XML attributes to the fragments inside your Frag2
After looking into this for a little, original question doesn't make much sense, I'd delete it but it got 2 upvotes ¯\_(ツ)_/¯
I solved my problem by adding a check before inflating graph, so that NavHostFragment's graph is set only if it doesn't already have one.
try {
navHostFragment.navController.graph
} catch (e: IllegalStateException) {
val inflater = navHostFragment.navController.navInflater
val graph = inflater.inflate(navigationId)
navHostFragment.navController.graph = graph
}
NavController.getGraph doesn't return null, instead it throws IllegalStateException, hence the weird check

Android navigation component: how save fragment state

I use bottomNavigationView and navigation component. Please tell me how I don't destroy the fragment after switching to another tab and return to the old one? For example I have three tabs - A, B, C. My start tab is A. After I navigate to B, then return A. When I return to tab A, I do not want it to be re-created. How do it? Thanks
As per the open issue, Navigation does not directly support multiple back stacks - i.e., saving the state of stack B when you go back to B from A or C since Fragments do not support multiple back stacks.
As per this comment:
The NavigationAdvancedSample is now available at https://github.com/googlesamples/android-architecture-components/tree/master/NavigationAdvancedSample
This sample uses multiple NavHostFragments, one for each bottom navigation tab, to work around the current limitations of the Fragment API in supporting multiple back stacks.
We'll be proceeding with the Fragment API to support multiple back stacks and the Navigation API to plug into it once created, which will remove the need for anything like the NavigationExtensions.kt file. We'll continue to use this issue to track that work.
Therefore you can use the NavigationAdvancedSample approach in your app right now and star the issue so that you get updates for when the underlying issue is resolved and direct support is added to Navigation.
In case you can deal with destroying fragment, but want to save ViewModel, you can scope it into the Navigation Graph:
private val viewModel: FavouritesViewModel by
navGraphViewModels(R.id.mobile_navigation) {
viewModelFactory
}
Read more here
EDIT
As #SpiralDev noted, using Hilt simplifies a bit:
private val viewModel: MainViewModel by
navGraphViewModels(R.id.mobile_navigation) {
defaultViewModelProviderFactory
}
just use navigation component version 2.4.0-alpha01 or above
Update:
Using last version of fragment navigation component, handle fragment states itself. see this sample
Old:
class BaseViewModel : ViewModel() {
val bundleFromFragment = MutableLiveData<Bundle>()
}
class HomeViewModel : BaseViewModel () {
... HomeViewModel logic
}
inside home fragment (tab of bottom navigation)
private var viewModel: HomeViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.bundleFromFragment.observe(viewLifecycleOwner, Observer {
val message = it.getString("ARGUMENT_MESSAGE", "")
binding.edtName.text = message
})
}
override fun onDestroyView() {
super.onDestroyView()
viewModel.bundleFromFragment.value = bundleOf(
"ARGUMENT_MESSAGE" to binding.edtName.text.toString(),
"SCROLL_POSITION" to binding.scrollable.scrollY
)
}
You can do this pattern for all fragments inside bottom navigation
Update 2021
use version 2.4.0-alpha05 or above.
don't use this answer or other etc.
This can be achieved using Fragment show/hide logic.
private val bottomFragmentMap = hashMapOf<Int, Fragment>()
bottomFragmentMap[0] = FragmentA.newInstance()
bottomFragmentMap[1] = FragmentB.newInstance()
bottomFragmentMap[2] = FragmentC.newInstance()
bottomFragmentMap[3] = FragmentD.newInstance()
private fun loadFragment(fragmentIndex: Int) {
val fragmentTransaction = childFragmentManager.beginTransaction()
val bottomFragment = bottomFragmentMap[fragmentIndex]!!
// first time case. Add to container
if (!bottomFragment.isAdded) {
fragmentTransaction.add(R.id.container, bottomFragment)
}
// hide remaining fragments
for ((key, value) in bottomFragmentMap) {
if (key == fragmentIndex) {
fragmentTransaction.show(value)
} else if (value.isVisible) {
fragmentTransaction.hide(value)
}
}
fragmentTransaction.commit()
}
Declare fragment on the activity & create fragment instance on onCreate method, then pass the fragment instance in updateFragment method. Create as many fragment instances as required corresponding to bottom navigation listener item id.
Fragment fragmentA;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home);
fragmentA = new Fragment();
updateFragment(fragmentA);
}
public void updateFragment(Fragment fragment) {
FragmentTransaction transaction =
getSupportFragmentManager().beginTransaction();
transaction.add(R.id.layoutFragment, fragment);
transaction.commit();
}
Furthermore be sure you are using android.support.v4.app.Fragment and calling getSupportFragmentManager()

Is there a way to keep fragment alive when using BottomNavigationView with new NavController?

I'm trying to use the new navigation component. I use a BottomNavigationView with the navController : NavigationUI.setupWithNavController(bottomNavigation, navController)
But when I'm switching fragments, they are each time destroy/create even if they were previously used.
Is there a way to keep alive our main fragments link to our BottomNavigationView?
Try this.
Navigator
Create custom navigator.
#Navigator.Name("custom_fragment") // Use as custom tag at navigation.xml
class CustomNavigator(
private val context: Context,
private val manager: FragmentManager,
private val containerId: Int
) : FragmentNavigator(context, manager, containerId) {
override fun navigate(destination: Destination, args: Bundle?, navOptions: NavOptions?) {
val tag = destination.id.toString()
val transaction = manager.beginTransaction()
val currentFragment = manager.primaryNavigationFragment
if (currentFragment != null) {
transaction.detach(currentFragment)
}
var fragment = manager.findFragmentByTag(tag)
if (fragment == null) {
fragment = destination.createFragment(args)
transaction.add(containerId, fragment, tag)
} else {
transaction.attach(fragment)
}
transaction.setPrimaryNavigationFragment(fragment)
transaction.setReorderingAllowed(true)
transaction.commit()
dispatchOnNavigatorNavigated(destination.id, BACK_STACK_DESTINATION_ADDED)
}
}
NavHostFragment
Create custom NavHostFragment.
class CustomNavHostFragment: NavHostFragment() {
override fun onCreateNavController(navController: NavController) {
super.onCreateNavController(navController)
navController.navigatorProvider += PersistentNavigator(context!!, childFragmentManager, id)
}
}
navigation.xml
Use custom tag instead of fragment tag.
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" android:id="#+id/navigation"
app:startDestination="#id/navigation_first">
<custom_fragment
android:id="#+id/navigation_first"
android:name="com.example.sample.FirstFragment"
android:label="FirstFragment" />
<custom_fragment
android:id="#+id/navigation_second"
android:name="com.example.sample.SecondFragment"
android:label="SecondFragment" />
</navigation>
activity layout
Use CustomNavHostFragment instead of NavHostFragment.
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:id="#+id/nav_host_fragment"
android:name="com.example.sample.CustomNavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="#+id/bottom_navigation"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="#navigation/navigation" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="#+id/bottom_navigation"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="#menu/navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
Update
I created sample project. link
I don't create custom NavHostFragment. I use navController.navigatorProvider += navigator.
Update 19.05.2021 Multiple backstack
Since Jetpack Navigation 2.4.0-alpha01 we have it out of the box.
Check Google Navigation Adavanced Sample
Old answer:
Google samples link
Just copy NavigationExtensions to your application and configure by example. Works great.
After many hours of research I found solution. It was all the time right in front of us :) There is a function: popBackStack(destination, inclusive) which navigate to given destination if found in backStack. It returns Boolean, so we can navigate there manually if the controller won't find the fragment.
if(findNavController().popBackStack(R.id.settingsFragment, false)) {
Log.d(TAG, "SettingsFragment found in backStack")
} else {
Log.d(TAG, "SettingsFragment not found in backStack, navigate manually")
findNavController().navigate(R.id.settingsFragment)
}
If you have trouble passing arguments add:
fragment.arguments = args
in class KeepStateNavigator
If you are here just to maintain the exact RecyclerView scroll state while navigating between fragments using BottomNavigationView and NavController, then there is a simple approach that is to store the layoutManager state in onDestroyView and restore it on onCreateView
I used ActivityViewModel to store the state. If you are using a different approach make sure you store the state in the parent activity or anything which survives longer than the fragment itself.
Fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerview.adapter = MyAdapter()
activityViewModel.listStateParcel?.let { parcelable ->
recyclerview.layoutManager?.onRestoreInstanceState(parcelable)
activityViewModel.listStateParcel = null
}
}
override fun onDestroyView() {
val listState = planet_list?.layoutManager?.onSaveInstanceState()
listState?.let { activityViewModel.saveListState(it) }
super.onDestroyView()
}
ViewModel
var plantListStateParcel: Parcelable? = null
fun savePlanetListState(parcel: Parcelable) {
plantListStateParcel = parcel
}
I've used the link provided by #STAR_ZERO and it works fine. For those who having problem with the back button, you can handle it in the activity / nav host like this.
override fun onBackPressed() {
if(navController.currentDestination!!.id!=R.id.homeFragment){
navController.navigate(R.id.homeFragment)
}else{
super.onBackPressed()
}
}
Just check whether current destination is your root / home fragment (normally the first one in bottom navigation view), if not, just navigate back to the fragment, if yes, only exit the app or do whatever you want.
Btw, this solution need to work together with the solution link above provided by STAR_ZERO, using keep_state_fragment.
Super easy solution for custom general fragment navigation:
Step 1
Create a subclass of FragmentNavigator, overwrite instantiateFragment or navigate as you need. If we want fragment only create once, we can cache it here and return cached one at instantiateFragment method.
Step 2
Create a subclass of NavHostFragment, overwrite createFragmentNavigator or onCreateNavController, so that can inject our customed navigator(in step1).
Step 3
Replace layout xml FragmentContainerView tag attribute from android:name="com.example.learn1.navigation.TabNavHostFragment" to your customed navHostFragment(in step2).
In the latest Navigation component release - bottom navigation view will keep track of the latest fragment in stack.
Here is a sample:
https://github.com/android/architecture-components-samples/tree/main/NavigationAdvancedSample
Example code
In project build.gradle
dependencies {
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.4.0-alpha01"
}
In app build.gradle
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'androidx.navigation.safeargs'
}
dependencies {
implementation "androidx.navigation:navigation-fragment-ktx:2.4.0-alpha01"
implementation "androidx.navigation:navigation-ui-ktx:2.4.0-alpha01"
}
Inside your activity - you can setup navigation with toolbar & bottom navigation view
val navHostFragment = supportFragmentManager.findFragmentById(R.id.newsNavHostFragment) as NavHostFragment
val navController = navHostFragment.navController
//setup with bottom navigation view
binding.bottomNavigationView.setupWithNavController(navController)
//if you want to disable back icon in first page of the bottom navigation view
val appBarConfiguration = AppBarConfiguration(
setOf(
R.id.feedFragment,
R.id.favoriteFragment
)
).
//setup with toolbar back navigation
binding.toolbar.setupWithNavController(navController, appBarConfiguration)
Now in your fragment, you can navigate to your second frgment & when you deselect/select the bottom navigation item - NavController will remember your last fragment from the stack.
Example: In your Custom adapter
adapter.setOnItemClickListener { item ->
findNavController().navigate(
R.id.action_Fragment1_to_Fragment2
)
}
Now, when you press back inside fragment 2, NavController will pop fragment 1 automatically.
https://developer.android.com/guide/navigation/navigation-navigate
Not available as of now.
As a workaround you could store all your fetched data into ViewModel and have that data readily available when you recreate the fragment. Make sure you get the ViewModel object using activity context.
You can use LiveData to make your data lifecycle-aware observable data holder.
The solution provided by #piotr-prus helped me, but I had to add some current destination check:
if (navController.currentDestination?.id == resId) {
return //do not navigate
}
without this check current destination is going to recreate if you mistakenly navigate to it, because it wouldn't be found in back stack.

Fragment instance is retained but child fragment is not re-attached

Update: accepted answer points to explanation (bug) with a work-around, but also see my Kotlin based work-around attached as an answer below.
This code is in Kotlin, but I think it is a basic android fragment life-cycle issue.
I have a Fragment that holds a reference to an other "subfragment"
Here is basically what I am doing:
I have a main fragment that has retainInstance set to true
I have a field in the main fragment that will hold a reference to the subfragment, initially this field is null
In the main fragment's onCreateView, I check to see if the subfragment field is null, if so, I create an instance of the subFragment and assign it to the field
Finally I add the subfragment to a container in the layout of the main fragment.
If the field is not null, ie we are in onCreateView due to a configuration change, I don't re-create the subfragment, I just try to added it to the containter.
When the device is rotated, I do observe the onPaused() and onDestroyView() methods of the subfragment being called, but I don't see any lifecyle methods being called on the subfragment during the process of adding the retained reference to the subfragment, to the child_container when the main fragments view is re-created.
The net affect is that I don't see the subfragment view in the main fragment. If I comment out the if (subfragment == null) and just create a new subfragment everytime, i do see the subfragment in the view.
Update
The answer below does point out a bug, in which the childFragmentManager is not retained on configuration changes. This will ultimately break my intended usage, which was to preserve the backstack after rotation, however I think what I am seeing is something different.
I added code to the activities onWindowFocusChanged method and I see something like this when the app is first launched:
activity is in view
fm = FragmentManager{b13b9b18 in Tab1Fragment{b13b2b98}}
tab 1 fragments = [DefaultSubfragment{b13bb610 #0 id=0x7f0c0078}]
and then after rotation:
activity is in view
fm = FragmentManager{b13f9c30 in Tab1Fragment{b13b2b98}}
tab 1 fragments = null
here fm is the childFragmentManager, and as you can see, we still have the same instance of Tab1Fragment, but it has a new childFragmentManager, which I think is unwanted and due to the bug reported in the answer below.
The thing is that I did add the subfragment to this new childFragmentManger.
So it seems like the transaction never executes with the reference to the fragment that was retained, but does complete if I create a brand new fragment. (I did try calling executePendingTransactions on the new childFragmentManager)
class Tab1Fragment: Fragment() {
var subfragment: DefaultSubfragment? = null
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val rootView = inflater!!.inflate(R.layout.fragment_main, container, false)
if (subfragment == null ) {
subfragment = DefaultSubfragment()
subfragment!!.sectionLabel = "label 1"
subfragment!!.buttonText = "button 1"
}
addRootContentToContainer(R.id.child_container, content = subfragment!!)
return rootView
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance = true
}
inline fun Fragment.addRootContentToContainer(containerId: Int, content: Fragment) {
val transaction = childFragmentManager.beginTransaction()
transaction.replace(containerId, content)
transaction.commit()
}
Your problem looks similar to the issue described here:
https://code.google.com/p/android/issues/detail?id=74222
unfortunately this issue will probably not be fixed by google.
Using retained fragments for UI or nested fragments is not a good idea - they are recomended to be used in place of onRetainNonConfigurationInstance, so ie. for large collections/data structures. Also you could find Loaders better than retained fragments, they also are retained during config changes.
btw. I find retained fragments more of a hack - like using android:configChanges to "fix" problems caused by screen rotations. It all works until user presses home screen and android decides to kill your app process. Once user will like to go back to your app - your retained fragments will be destroyed - and you will still have to recreate it. So its always better to code everything like if your resources could be destroyed any time.
The accepted answer to my question above points out a reported bug in the support library v4 in which nested fragments (and child fragment managers) are no longer retained on configuration changes.
One of the posts provides a work-around (which seems to work well).
The work around involves creating a subclass of Fragment and uses reflection.
Since my original question used Kotlin code, I thought I would share my Kotlin version of the work around here in case anyone else hits this. In the end, I am not sure I will stick with this solution, since it is still somewhat of a hack, it still manipulates private fields, however if the field name is changed, the error will be found at compile time rather than runtime.
The way this works is this:
In your fragment that will contain child fragments you create a field retainedChildFragmentManager, that will hold the childFragmentManager that will be lost during the configuration change
In the onCreate callback for the same fragment, you set retainInstance to true
In the onAttach callback for the same fragment, you check to see if retainedChildFragmentManger is non-null, if so you call a Fragment extension function that re-attaches the retainedChildFragmentManager, otherwise you set the retainedChildFragmentManager to the current childFragmentManager.
Finally you need to fix the child fragments to point back to the newly created hosting activity (the bug leaves them referencing the old activity, which I think results in a memory leak).
Here is an example:
Kotlin Fragment extensions
// some convenience functions
inline fun Fragment.pushContentIntoContainer(containerId: Int, content: Fragment) {
val transaction = fragmentManager.beginTransaction()
transaction.replace(containerId, content)
transaction.addToBackStack("tag")
transaction.commit()
}
inline fun Fragment.addRootContentToContainer(containerId: Int, content: Fragment) {
val transaction = childFragmentManager.beginTransaction()
transaction.replace(containerId, content)
transaction.commit()
}
// here we address the bug
inline fun Fragment.reattachRetainedChildFragmentManager(childFragmentManager: FragmentManager) {
setChildFragmentManager(childFragmentManager)
updateChildFragmentsHost()
}
fun Fragment.setChildFragmentManager(childFragmentManager: FragmentManager) {
if (childFragmentManager is FragmentManagerImpl) {
mChildFragmentManager = childFragmentManager // mChildFragmentManager is private to Fragment, but the extension can touch it
}
}
fun Fragment.updateChildFragmentsHost() {
mChildFragmentManager.fragments.forEach { fragment -> // fragments is hidden in Fragment
fragment?.mHost = mHost // mHost is private also
}
}
The Fragment Hosting the child Fragments
class Tab1Fragment : Fragment() , TabRootFragment {
var subfragment: DefaultSubfragment? = null
var retainedChildFragmentManager: FragmentManager? = null
override val title = "Tab 1"
override fun onCreateView(inflater: LayoutInflater?, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
val rootView = inflater!!.inflate(R.layout.fragment_main, container, false)
if (subfragment == null ) {
subfragment = DefaultSubfragment()
subfragment!!.sectionLable = "label 1x"
subfragment!!.buttonText = "button 1"
addRootContentToContainer(R.id.child_container, content = subfragment!!)
}
return rootView
}
override fun onAttach(context: Context?) {
super.onAttach(context)
if (retainedChildFragmentManager != null) {
reattachRetainedChildFragmentManager(retainedChildFragmentManager!!)
} else {
retainedChildFragmentManager = childFragmentManager
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance = true
}
}

Categories

Resources