Kotlin 委托中断导航

Kotlin delegate disrupting Navigation

我正在尝试 Jetpack Navigation component 并设置了一个非常基本的导航图,其中只有 2 个片段,其中一个主页片段 (Foo) 包含一个按钮,该按钮调用导航操作以打开另一个片段 (Bar).

只有基本的 Android 用法和功能按预期工作,我可以通过按后退按钮导航回 Foo 并再次向前导航至 Bar

我实现了这个方便 delegate class 以我喜欢的方式通过 id 绑定视图(我最初是 iOS 开发者)。

class FindViewById<in R, T: View>(private val id: Int) {

    private var view: T? = null

    operator fun getValue(thisRef: R, property: KProperty<*>): T {
        var view = this.view
        if (view == null) {
            view = when (thisRef) {
                is Activity -> thisRef.findViewById(id)!!
                is Fragment -> thisRef.requireView().findViewById(id)!!
                is View -> thisRef.findViewById(id)!!
                else -> throw NullPointerException()
            }
            this.view = view // Comment out to never cache reference
        }
        return view
    }
}

这让我可以像这样写代码

class FragmentFoo: Fragment() {
    
    private val textView: TextView by FindViewById(R.id.text_view)
    private val button: Button by FindViewById(R.id.button)
    ...

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        button.setOnClickListener { 
            findNavController().navigate(R.id.action_foo_to_bar)
        }
    }
}

现在突然间,当我导航到 Bar 然后按后退按钮时,我再次到达 Foo 但我无法向前导航到 Bar。如果我删除 FindViewById 中的 this.view = view 行,它会再次起作用。

我的猜测是存在一些与内存相关的问题,尽管我尝试将 view 包装在 WeakReference 中,但它并没有解决问题。

我认为在委托中缓存找到的视图在性能方面是个好主意。

知道为什么会发生这种情况以及如何在缓存找到的视图时解决问题吗?

编辑

我的目的不是找到另一种引用视图的方式,而是为什么这个委托实现会破坏导航组件,所以如果我将来要创建另一个自定义委托,我就不会再遇到它了。

解决方案

is Fragment -> {
    thisRef.viewLifecycleOwnerLiveData.value!!.lifecycle.addObserver(object: LifecycleEventObserver {
        override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
            if (event == Lifecycle.Event.ON_STOP) this@FindViewById.view = null
        }
    })
    return thisRef.requireView().findViewById(id)!!
}

在 android Fragment 中,视图有自己定义明确的 lifecycle 并且此生命周期独立于片段的生命周期进行管理。

当您使用导航组件时,它使用 fragment replace transaction under the hood and adds the previous fragment to the back stack. At this point this fragment goes into CREATED state and as you can see on this diagram 它的视图实际上被破坏了。此时您的委托仍然保留对该旧视图层次结构的引用,导致内存泄漏。

稍后,当您向后导航时,片段通过 STARTED 返回到 RESUMED 状态,但是 视图层次结构被重新创建 - onCreateViewonViewCreated 方法在这个过程中被再次调用。因此,虽然片段显示了一个全新的视图层次结构,但您的委托仍然引用旧视图层次结构。

因此,如果您想手动缓存任何视图引用,则需要覆盖 onDestroyView 并清除这些引用以避免内存泄漏和此类不正确的行为。同样对于这个特殊问题,我建议使用 ViewBinding.

如果您想拥有自己的实现,但又不想清除 onDestroyView 中的引用(例如,因为它破坏了很好的独立抽象),viewLifecycleOwnerLiveData 可能会有用观察当前视图状态并在视图被销毁时清除所有引用。

请查看 fragments 文档,它最近更新并涵盖了片段的大部分方面。