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

时间:2016-01-02 18:52:58

标签: android android-fragments kotlin

更新:通过解决方案接受了解释(错误)的答案点,但也看到我的基于Kotlin的解决方案作为下面的答案附加。

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

我有一个片段,其中包含对其他“subfragment”的引用

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

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

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

    更新

    下面的答案确实指出了一个错误,其中未在配置更改中保留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()
    }
    

2 个答案:

答案 0 :(得分:2)

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

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

不幸的是,谷歌可能无法修复此问题。

将保留的片段用于UI或嵌套片段不是一个好主意 - 建议使用它们代替onRetainNonConfigurationInstance,即ie。适用于大型馆藏/数据结构。您还可以找到比保留片段更好的Loaders,它们也会在配置更改期间保留。

顺便说一句。我发现保留的片段更像是一个黑客 - 比如使用android:configChanges来“修复”由屏幕旋转引起的问题。这一切都有效,直到用户按下主屏幕并且android决定杀死你的应用程序进程。一旦用户想要返回您的应用程序 - 您保留的片段将被销毁 - 您仍然需要重新创建它。因此,如果您的资源可以随时被销毁,那么编码所有内容总是更好。

答案 1 :(得分:2)

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

其中一个posts提供了解决方法(似乎效果很好)。 解决方法涉及创建Fragment的子类并使用反射。

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

这种方式是这样的:

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

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