Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlin delegate disrupting Navigation

I'm trying Jetpack Navigation component and have set up a very basic navigation graph with just 2 Fragments with one home fragment (Foo) containing a button which calls a navigation action to open the other fragment (Bar).

With only the basic Android usage and functions it works as intended, I can navigate back to Foo by pressing the back button and navigate forward to Bar again.

I implemented this convenience delegate class for binding views by id in my preferred way (Im originally an iOS dev).

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

This allows me to write code like this

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

Now all of a sudden when I navigate to Bar and then press the back button I arrive at Foo again but I cannot navigate forward to Bar. If I remove the line this.view = view in FindViewById it works again.

My guess is there is some memory related issue, though I tried wrapping the view inside a WeakReference but it didn't solve the problem.

I think it is a good idea performance-wise to cache the found view in the delegate.

Any idea why this is occurring and how I can resolve the problem while caching the found view?

Edit

My intention is not to find another way of referencing views but rather why this delegate implementation disrupts the Navigation component so I don't experience it again if I were to make another custom delegate in the future.

Solution

is Fragment -> {
    thisRef.viewLifecycleOwnerLiveData.value!!.lifecycle.addObserver(object: LifecycleEventObserver {
        override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
            if (event == Lifecycle.Event.ON_STOP) [email protected] = null
        }
    })
    return thisRef.requireView().findViewById(id)!!
}
like image 313
Arbitur Avatar asked Jan 24 '23 14:01

Arbitur


1 Answers

In android Fragment view has its own well-defined lifecycle and this lifecycle is managed independently from that of the fragment's Lifecycle.

When you are using navigation component it uses a 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 its view is actually destroyed. At this point your delegate still keeps the reference to this old view hierarchy, causing a memory leak.

Later, when you are navigating back, the fragment goes back through STARTED into RESUMED state, but the view hierarchy is recreated - onCreateView and onViewCreated methods are called again during this process. So while the fragment displays a completely new view hierarchy, your delegate still references the old one.

So if you'd like to manually cache any view references, you need to override onDestroyView and clear these references to avoid memory leaks and this kind of incorrect behavior. Also for this particular problem I'd recommend using ViewBinding.

If you'd like to have your own implementation, but do not like to clear references in onDestroyView (e.g. because it breaks nice and self-contained abstraction), viewLifecycleOwnerLiveData might be useful to observe current view state and clear all references when view is destroyed.

Please check out the fragments documentation, it has been recently updated and covers most aspects of fragments.

like image 94
esentsov Avatar answered Jan 28 '23 06:01

esentsov