Android 片段实例被保留,但子片段未重新附加

Android 片段实例被保留,但子片段未重新附加,android,android-fragments,kotlin,Android,Android Fragments,Kotlin,更新:接受的答案指向解释(bug)和解决方法,但也可以参见下面作为答案附加的基于Kotlin的解决方法 这段代码在Kotlin中,但我认为这是一个基本的android片段生命周期问题 我有一个片段包含对另一个“子片段”的引用 以下是我正在做的事情: 我有一个主片段,retainInstance设置为true 我在主片段中有一个字段,它将保存对子片段的引用,最初这个字段为null 在主片段的onCreateView中,我检查子片段字段是否为null,如果为null,我创建子片段的实例并将其分配给该

更新:接受的答案指向解释(bug)和解决方法,但也可以参见下面作为答案附加的基于Kotlin的解决方法

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

我有一个片段包含对另一个“子片段”的引用

以下是我正在做的事情:

  • 我有一个主片段,
    retainInstance
    设置为true
  • 我在主片段中有一个字段,它将保存对子片段的引用,最初这个字段为null
  • 在主片段的
    onCreateView
    中,我检查子片段字段是否为null,如果为null,我创建子片段的实例并将其分配给该字段
  • 最后,我将子片段添加到主片段布局中的容器中
  • 如果该字段不为null,即由于配置更改而处于onCreateView中,我不会重新创建子片段,我只是尝试将其添加到容器中
  • 当设备旋转时,我确实观察到正在调用的子片段的
    onPaused()
    onDestroyView()
    方法,但是在将保留引用添加到子片段的过程中,我没有看到在子片段上调用任何生命周期方法,当重新创建主片段视图时,添加到子容器

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

    更新 下面的答案确实指出了一个bug,在这个bug中,配置更改时不会保留childFragmentManager。这将最终打破我的预期用途,即在旋转后保留回撤,但我认为我看到的是不同的东西

    我在activities
    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,我认为这是不需要的,这是由于下面的答案中报告的错误。 问题是,我确实将子片段添加到了这个新的子片段管理器中。 因此,事务似乎永远不会在引用保留的片段时执行,但如果我创建一个全新的片段,事务就会完成。(我尝试在新的childFragmentManager上调用
    executePendingTransactions



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

    不幸的是,谷歌可能无法解决这个问题

    将保留片段用于UI或嵌套片段不是一个好主意-建议使用它们来代替OnRetainOnConfiguration实例,例如用于大型集合/数据结构。此外,您可以找到比保留片段更好的加载程序,它们在配置更改期间也会保留


    顺便说一句,我发现保留的片段更像是使用
    android:configChanges
    来“修复”屏幕旋转引起的问题。直到用户按下主屏幕,安卓决定终止你的应用程序进程,所有这些都会起作用。一旦用户想要回到你们的应用程序,你们保留的片段将被销毁,你们仍然需要重新创建它。因此,最好对所有内容进行编码,就像您的资源随时可能被破坏一样。

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

    其中一种方法提供了一种变通方法(似乎效果很好)。 解决方法包括创建Fragment的子类并使用反射

    因为我最初的问题使用了Kotlin代码,所以我想我会在这里分享我的Kotlin版本的工作,以防其他人碰到这个问题。最后,我不确定我是否会坚持使用这个解决方案,因为它仍然是一种黑客行为,它仍然会操纵私有字段,但是如果字段名称发生更改,那么错误将在编译时而不是运行时发现

    其工作方式如下:

  • 在包含子片段的片段中,您创建了一个字段retainedChildFragmentManager,该字段将保存配置更改期间丢失的childFragmentManager
  • 在同一片段的onCreate回调中,将retainInstance设置为true
  • 在同一片段的onAttach回调中,检查retainedChildFragmentManager是否为非null,如果为非null,则调用重新附加retainedChildFragmentManager的片段扩展函数,否则将retainedChildFragmentManager设置为当前子片段管理器
  • 最后,您需要修复子片段以指向新创建的宿主活动(该错误使它们引用旧活动,我认为这会导致内存泄漏)
  • 以下是一个例子:

    Kotlin片段扩展 托管子片段的片段
    我会仔细看看你发布的链接。我不是100%确定这是我遇到的同一个问题。顺便说一句,我真正想做的就是有几个标签,每个标签都有自己的背景。我见过很多涉及管理自己的后台堆栈的解决方案,但它们都有点陈旧,在大多数情况下,我认为嵌套片段似乎可以工作(每个选项卡都有自己的childFragmentManager和自己的后台堆栈),现在是处理设备旋转的关键部分。我现在不接受这个,我确实看到了错误,最终它将阻止我做我计划的事情,但这似乎是另一回事。我将补充我的问题我遇到的问题是r
    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()
    }
    
    // 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
        }    
    }