片段实例被保留,但子片段不被重新连接

更新:接受的解答(bug)与答案的答案点,但也看到我的Kotlin基于解决方法作为答案下面。

这个代码在Kotlin中,但是我认为这是一个基本的Android片段生命周期问题。

我有一个片段,其中提到了另一个“子片段”

这里基本上是我在做什么:

  1. 我有一个retainInstance设置为true的主要片段
  2. 我在主片段中有一个字段,将保存对子片段的引用,最初这个字段为空
  3. 在主片段的onCreateView ,我检查是否subfragment字段为空,如果是这样,我创建一个subFragment的实例,并将其分配给字段
  4. 最后,我将子片段添加到主片段布局中的容器中。
  5. 如果该字段不为空,即由于配置更改,我们在onCreateView中,则不会重新创建子片段,只是尝试将其添加到包含器中。

当设备旋转时,我确实观察到被调用的子片段的onPaused()onDestroyView()方法,但是在将保留的引用添加到子片段的过程中,没有看到在子片段上调用的任何lifecyle方法,在重新创建主片段视图时将其添加到child_container。

净影响是,我没有看到主片段中的亚片段视图。 如果我注释掉if(subfragment == null),并且每次创建一个新的子片段,我都会在视图中看到子片段。

更新

下面的答案指出了一个bug,其中childFragmentManager不保留配置更改。 这将最终打破我的预期用法,即在旋转后保留后台,但我认为我所看到的是不同的东西。

我将代码添加到onWindowFocusChanged方法的活动中,并在应用程序第一次启动时看到如下所示的内容:

 activity is in view fm = FragmentManager{b13b9b18 in Tab1Fragment{b13b2b98}} tab 1 fragments = [DefaultSubfragment{b13bb610 #0 id=0x7f0c0078}] 

然后旋转后:

 activity is in view fm = FragmentManager{b13f9c30 in Tab1Fragment{b13b2b98}} tab 1 fragments = null 

这里的fm是childFragmentManager,正如你所看到的,我们仍然有Tab1Fragment的实例,但它有一个新的childFragmentManager,我认为这是不需要的,并且由于下面的答案中报告的错误。 事情是,我没有添加子片段到这个新的 childFragmentManger。 所以看起来事务不会执行保留片段的引用,但是如果我创建了一个全新的片段,事务就完成了。 (我尝试在新的childFragmentManager上调用executePendingTransactions)


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

您的问题与此处所述的问题类似:

https://code.google.com/p/android/issues/detail?id=74222

不幸的是,这个问题可能不会被谷歌修复。

使用保留的片段用于UI或嵌套片段不是一个好主意 – 它们被推荐用来代替onRetainNonConfigurationInstance,所以也就是说。 用于大型集合/数据结构。 你也可以发现装载机比保留的碎片更好,它们也在配置更改期间保留。

顺便说一句。 我发现保留的碎片更像一个黑客 – 使用android:configChanges来“修复”由屏幕旋转引起的问题。 它一直工作,直到用户按主屏幕和Android决定杀死你的应用程序进程。 一旦用户想回到你的应用程序 – 你保留的碎片将被破坏 – 你仍然需要重新创建它。 因此,如果您的资源随时可能被破坏,那么最好对代码进行编码。

上述问题的接受答案指出了支持库v4中的一个报告错误,其中嵌套的片段(和子片段管理器)不再保留在配置更改上。

其中一个职位提供了一个解决方法(这似乎工作得很好)。 解决方法包括创建片段的子类并使用反射。

由于我原来的问题使用Kotlin代码,所以我想我会在这里分享我的Kotlin版本的工作,以防其他人点击这个。 最后,我不确定我会坚持这个解决方案,因为它仍然是一个黑客,它仍然操纵私人领域,但是,如果字段名称更改,错误将在编译时,而不是运行时找到。

这个工作的方式是这样的:

  1. 在将包含子片段的片段中,您将创建一个字段retainChildFragmentManager,该字段将保存将在配置更改期间丢失的childFragmentManager
  2. 在相同片段的onCreate回调函数中,将retainInstance设置为true
  3. 在同一个片段的onAttach回调函数中,检查retainChildFragmentManger是否为非null,如果是,则调用一个重新连接retainChildFragmentManager的Fragment扩展函数,否则将retainChildFragmentManager设置为当前的childFragmentManager。
  4. 最后,您需要修复子片段以指向新创建的托管活动(该错误使他们引用旧的活动,我认为这会导致内存泄漏)。

这里是一个例子:

Kotlin片段扩展

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

主持孩子片段的片段

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