Thursday, March 14, 2019

Auto views references release for Fragments

Take a look at this code:

class MyFragment : Fragment() {

    private var someView: View? = null

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.f_my_fragment, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        someView = view.findViewById(R.id.view)
    }

}

What do you think? Is everything fine here? I'll give you a minute :)
Nope, there is a memory leak. Our someView variable will become a memory leak when fragment goes to back stack. Ideally we must remove all references to views in onDestroyView() method:

class MyFragment : Fragment() {

    private var someView: View? = null

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.f_my_fragment, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        someView = view.findViewById(R.id.view)
    }

    override fun onDestroyView() {
        super.onDestroyView()
        
        someView = null
    }

}

Now we have fixed this memory leak. But this process can become cumbersome when we deal with multiple views. Also it is pretty easy to forget to release all references.

But we can do better!

So I wrote this property delegate which automatically releases all references to views:

private class FragmentViewProperty<T : View> : ReadWriteProperty<Fragment, T>, LifecycleObserver {

    private var view: T? = null
    private var lifecycle: Lifecycle? = null

    override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
        return view ?: throw NullPointerException()
    }

    override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) {
        view = value

        lifecycle?.removeObserver(this)
        lifecycle = thisRef.viewLifecycleOwner.lifecycle.also {
            it.addObserver(this)
        }
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    private fun onViewDestroyed() {
        view = null
        lifecycle?.removeObserver(this)
        lifecycle = null
    }
}

fun <T: View> fragmentView(): ReadWriteProperty<Fragment, T> = FragmentViewProperty()

And now we can rewrite our code:

class MyFragment : Fragment() {

    private var someView by fragmentView<View>()

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        return inflater.inflate(R.layout.f_my_fragment, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        someView = view.findViewById(R.id.view)
        someView.requestFocus() // as an added benefit we don't need to do null-checks now
    }
}

Hope this was helpful. Cheers.