保留片段实例,但不重新附加子片段

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

更新: 接受的答案指向带有变通方法的解释(bug),但也可以在下面看到我的基于 Kotlin 的变通方法作为答案。

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

我有一个 Fragment 引用了另一个 "subfragment"

这基本上是我正在做的事情:

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

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

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

更新

下面的答案确实指出了一个错误,其中 childFragmentManager 在配置更改时没有保留。这最终会破坏我的预期用途,即在轮换后保留 backstack,但我认为我看到的是不同的东西。

我将代码添加到活动 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,我认为这是不需要的,因为下面的答案中报告了错误。 问题是我 did 将子片段添加到这个 new 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

不幸的是,这个问题可能不会被 google 解决。

为 UI 使用保留片段或嵌套片段不是一个好主意 - 建议使用它们代替 onRetainNonConfigurationInstance,即。对于大型 collections/data 结构。您还可以发现 Loaders 比保留片段更好,它们在配置更改期间也会保留。

顺便说一句。我发现保留的片段更像是一种 hack - 比如使用 android:configChanges 到 "fix" 屏幕旋转引起的问题。这一切都有效,直到用户按下主屏幕并且 android 决定终止您的应用程序进程。一旦用户想要返回您的应用程序——您保留的片段将被销毁——您仍然需要重新创建它。因此,如果您的资源随时可能被破坏,最好对所有内容进行编码。

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

其中一个 posts 提供了 work-around(这似乎工作得很好)。 解决方法涉及创建 Fragment 的子类并使用反射。

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

它的工作方式是这样的:

  1. 在您将包含子片段的片段中,您创建了一个字段 retainedChildFragmentManager,它将保存将在配置更改期间丢失的 childFragmentManager
  2. 在同一片段的 onCreate 回调中,您将 retainInstance 设置为 true
  3. 在同一片段的 onAttach 回调中,检查 retainedChildFragmentManger 是否为 non-null,如果是,则调用 re-attaches retainedChildFragmentManager 的片段扩展函数,否则将 retainedChildFragmentManager 设置为当前的 childFragmentManager.
  4. 最后,您需要修复子片段以指向新创建的宿主 activity(错误使它们引用旧的 activity,我认为这会导致内存泄漏)。

这是一个例子:

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