片段实例被保留,但子片段不被重新连接
更新:接受的解答(bug)与答案的答案点,但也看到我的Kotlin基于解决方法作为答案下面。
这个代码在Kotlin中,但是我认为这是一个基本的Android片段生命周期问题。
我有一个片段,其中提到了另一个“子片段”
这里基本上是我在做什么:
- 我有一个
retainInstance
设置为true的主要片段 - 我在主片段中有一个字段,将保存对子片段的引用,最初这个字段为空
- 在主片段的
onCreateView
,我检查是否subfragment字段为空,如果是这样,我创建一个subFragment的实例,并将其分配给字段 - 最后,我将子片段添加到主片段布局中的容器中。
- 如果该字段不为空,即由于配置更改,我们在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版本的工作,以防其他人点击这个。 最后,我不确定我会坚持这个解决方案,因为它仍然是一个黑客,它仍然操纵私人领域,但是,如果字段名称更改,错误将在编译时,而不是运行时找到。
这个工作的方式是这样的:
- 在将包含子片段的片段中,您将创建一个字段retainChildFragmentManager,该字段将保存将在配置更改期间丢失的childFragmentManager
- 在相同片段的onCreate回调函数中,将retainInstance设置为true
- 在同一个片段的onAttach回调函数中,检查retainChildFragmentManger是否为非null,如果是,则调用一个重新连接retainChildFragmentManager的Fragment扩展函数,否则将retainChildFragmentManager设置为当前的childFragmentManager。
- 最后,您需要修复子片段以指向新创建的托管活动(该错误使他们引用旧的活动,我认为这会导致内存泄漏)。
这里是一个例子:
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 } }
- Android测试失败时,Gradle构建失败,“字符串索引超出范围”
- Kotlin Higher Order Function以可变数量的参数作为parameter passing一个函数
- 如何以编程方式将SelectableItemBackground添加到ImageButton?
- 当运行测试android嘲笑时,在视图中的空指针
- 无法让dokka在gradle / android项目上生成kotlin文档
- 用MVP + Dagger进行Android测试2
- 我可以构建这个测验客户端应用程序吗?
- 致命exception:与Klaxon json Kotlin库的java.lang.RuntimeException
- 在kotlin中定制ProgressBar