Photo by Juliana Kozoski on Unsplash
In Android development, forgetting to remove listeners can often lead to memory leaks. Anonymous classes or lambdas holding a reference to a Fragment long after its View is destroyed can silently block precious amounts of memory from being reclaimed. However, cleaning up after ourselves manually every single time is not only cumbersome but error-prone too.
A modern solution to the problem could be the creation of lifecycle-aware components. In this article, I’ll show you an example of my proposed implementation using ViewPager2’s onPageSelected callback, but first, let us quickly walk through the old way of dealing with this issue.
The old way
We start by declaring a listener. Depending on the use-case it can be nullable or lateinit var if it works with anything non-global, or a private property in the Fragment:
private val onPageChangeCallback = object : OnPageChangeCallback() { | |
override fun onPageSelected(position: Int) { | |
// Do stuff | |
} | |
} |
We can then register it in a lifecycle event before the screen appears and remove it in the corresponding lifecycle event after the screen disappears:
override fun onStart() { | |
super.onStart() | |
binding.pager.registerOnPageChangeCallback(onPageChangeCallback) | |
} | |
override fun onStop() { | |
super.onStop() | |
binding.pager.unregisterOnPageChangeCallback(onPageChangeCallback) | |
} |
The plan is to get rid of these callbacks and move the registering and unregistering parts closer to each other, making sure that calling the first one will automatically result in calling the second one when needed.
The new way, aka Lifecycle to the rescue!
The easy way of simplifying and improving the above implementation would be to let Lifecycle deal with the dirty work of adding and removing the listener, with our code looking something like this:
binding.pager.onPageSelected(viewLifecycleOwner) { position ->
// Do stuff
}
To get there first, we create the following extension function for ViewPager2:
fun ViewPager2.onPageSelected( lifecycleOwner: LifecycleOwner, listener: (position: Int) -> Unit )
By marking it inline, we make sure to avoid the runtime penalty of unnecessary memory allocation.
We can implement the callback interface to hook in our lambda:
inline fun ViewPager2.onPageSelected2(lifecycle: Lifecycle, | |
crossinline listener: (position: Int) -> Unit) { | |
val onPageChangeCallback = object : OnPageChangeCallback() { | |
override fun onPageSelected(position: Int) = listener(position) | |
} | |
// | |
} |
…and make the component lifecycle-aware so that it will handle registering and unregistering itself.
object : DefaultLifecycleObserver { | |
init { | |
lifecycleOwner.lifecycle.addObserver(this) | |
} | |
override fun onStart(owner: LifecycleOwner) { | |
registerOnPageChangeCallback(onPageChangeCallback) | |
} | |
override fun onStop(owner: LifecycleOwner) { | |
unregisterOnPageChangeCallback(onPageChangeCallback) | |
} | |
} |
There, that’s it. It does the job, but there’s still some room for improvement. If you’d rather use lambdas for the lifecycle callbacks to improve readability and brevity, the content-aware parts can be moved into a helper class to serve this purpose:
class LifecycleEventDispatcher( | |
lifecycleOwner: LifecycleOwner, | |
val onCreate: () -> Unit = {}, | |
val onStart: () -> Unit = {}, | |
val onResume: () -> Unit = {}, | |
val onPause: () -> Unit = {}, | |
val onStop: () -> Unit = {}, | |
val onDestroy: () -> Unit = {} | |
) : DefaultLifecycleObserver { | |
init { lifecycleOwner.lifecycle.addObserver(this) } | |
override fun onCreate(owner: LifecycleOwner) = onCreate() | |
override fun onStart(owner: LifecycleOwner) = onStart() | |
override fun onResume(owner: LifecycleOwner) = onResume() | |
override fun onPause(owner: LifecycleOwner) = onPause() | |
override fun onStop(owner: LifecycleOwner) = onStop() | |
override fun onDestroy(owner: LifecycleOwner) = onDestroy() | |
} |
Wrapping it all up we’ll end up with the following:
inline fun ViewPager2.onPageSelected(lifecycleOwner: LifecycleOwner, | |
crossinline listener: (position: Int) -> Unit) { | |
object : OnPageChangeCallback() { | |
override fun onPageSelected(position: Int) = listener(position) | |
}.let { | |
LifecycleEventDispatcher(lifecycleOwner, | |
onStart = { registerOnPageChangeCallback(it) }, | |
onStop = { unregisterOnPageChangeCallback(it) }) | |
} | |
} |
So we can go ahead and use this extension function anywhere, without having to worry about manually getting rid of the listener and leaking memory.
Approaching similar problems in this lifecycle-aware way results in slightly shorter, more readable, and safer presentation classes.
That being said, I’m leaving you with the above steps, so you can go ahead and try them for yourself. Questions, suggestions, or any examples in which this proposed solution fixed your memory leak problems are welcome and encouraged, so feel free to share them in the comments.
Happy coding!
Leave A Comment