Today a mate asked me how he could do an OnGlobalLayoutListener
properly without incurring in the need of too much boilerplate. This was a tricky question because of a couple of things, let’s see it a little more deeply.
What is OnGlobalLayoutListener for?
This listener is available for any view’s ViewTreeObserver
and it’s quite often used to get a callback when the view is inflated and measured, and we already have a width and height available to do any kind of calculations, animations, etc.
If you’re going to use this method, make sure it’s really what you need. Check this tweet from Chris Banes
OnGlobalLayoutListener is *massively* overkill 99% of the time. That is why android-ktx has doOnLayout() and doOnNextLayout() which use View.OnLayoutChangeListener.
— Chris Banes (@chrisbanes) April 17, 2018
Thanks to the awesome Java interoperability that Kotlin provides, we can do this on a very clean way using its simulated properties and lambdas for single-method interfaces:
recycler.viewTreeObserver.addOnGlobalLayoutListener { // do whatever }
What’s the issue here? To prevent leaks, a recommended practice is to remove the listener once you’ve finished using it. But we don’t have a reference to the object because we used a lambda, and a lambda is not exactly the same as an object.
We could still use the old-fashioned style, but a kitten dies every time we use an anonymous object directly in Kotlin. We are not changing to a nicer language if we still need to do things like this:
recycler.viewTreeObserver.addOnGlobalLayoutListener( object : ViewTreeObserver.OnGlobalLayoutListener { override fun onGlobalLayout() { recycler.viewTreeObserver.removeOnGlobalLayoutListener(this); // do whatever } });
Finding a better alternative
Ok, we know we don’t want that. But what can we do to make it better? We are forced to use the not-so-good-looking way, but a good alternative would be to hide this behind an extension function.
We will then create a new function for views that receives another function and creates and removes the listener by itself. Something like:
inline fun View.waitForLayout(crossinline f: () -> Unit) = with(viewTreeObserver) { addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { override fun onGlobalLayout() { removeOnGlobalLayoutListener(this) f() } }) }
You can now just call the function and be sure that the listener is added and removed by itself. Besides, you’ll never forget about removing it anymore:
recycler.waitForLayout { // do whatever }
If you prefer, you could apply the extension function to the ViewTreeObserver
instead of directly to the View
. That’s up to you.
But we can improve it
This layout listener is usually used to do something after a view is measured, so you typically would need to wait until width and height are greater than 0. And we probably want to do something with the view that called it, so why don’t we convert the parameter function into an extension function too?
I also generified the function so that it can be used by any object that extends View and also be able to access to all its specific functions and properties from the function we’ll write.
inline fun T.afterMeasured(crossinline f: T.() -> Unit) { viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { override fun onGlobalLayout() { if (measuredWidth > 0 && measuredHeight > 0) { viewTreeObserver.removeOnGlobalLayoutListener(this) f() } } }) }
This afterMeasured
function is very similar to the previous one, but you can use the properties and public methods of the view directly inside the lambda. We can, for instance, get the width of the recycler and set a layout with a dynamic number of columns depending on it.
recycler.afterMeasured { val columnCount = width / columnWidth layoutManager = GridLayoutManager(context, columnCount) }
Conclusion
It’s true that there are still some things that don’t look nice when working with Android, even moving to Kotlin, but we can always find an alternative that improves readability and avoids boilerplate, by hiding this boilerplate behind other structures. At least you’ll have to only write it once and the rest of the code will look awesome!
Nice progression through building Kotlin tooling for this (obnoxiously still-present) use case!
It’s worth mentioning in the same breath (or maybe this was going to be your part (II)…), that if support for pre-11 devices is not needed (which is pretty common, these days), this particular problem can be avoided by using `View.addOnLayoutChangeListener(listener: View.OnLayoutChangeListener)`. These listeners receive the changed view directly as a parameter, so it’s easy to remove the listener after one firing, or at the end of the calling Context’s lifecycle, or not at all.
When it activates, the View is still in the layout pass, so it can present some other interesting sorts of problems, depending on what changes are actually needed. Particularly, doing anything that causes the view to lay itself out again can lead to shenanigans, if one is not careful.
You mean that it’d be easier to remove the listener because you get the view, but in the old-fashioned way right? Because using lambdas would have the same problem, unless I’m missing something. But yeah, you’re right, it’s probably better to use `onLayoutChangedListener` instead. The example wouldn’t have been that fun btw ;P Thanks for your comment!
Very nice idea. Your series on Kotlin are fun to read. One question:
if (measuredWidth > 0 && measuredHeight > 0) {
removeOnGlobalLayoutListener(this)
Are you sure you’d want removing the layout listener to be dependent on whether the view reaches non-zero dimensions? Shouldn’t this go outside of the if? I mean, if this never happens (for whatever reason), the listener gets stuck forever, no?
Yeah, don’t take the example as the best Android code forever, just an idea of what can be done using Kotlin. By the way, there are cases where this listener is called and the view is still not measured, so it could be necessary. In general, this listener must be used carefully, so it could need to be optimized for the particular case where it’s used.
I applied your inline function to a view in onCreate():
inline fun View.waitForLayout(crossinline f: () -> Unit) = with(viewTreeObserver) {
addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
removeOnGlobalLayoutListener(this)
f()
}
})
}
but an exception was thrown which states that the viewtreeobserver is not alive
so I had to modify the function without using with(viewTreeObserver) like so:
inline fun View.waitForLayout(crossinline f: () -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
viewTreeObserver.removeOnGlobalLayoutListener(this)
f()
}
})
}
Oh weird. Thanks, hope it helps others.
Very interesting post, the approach for extending the view is very straight forward.
Although for some reason the onGlobalLayout method is never called on my application… None of my tries to use GlobalLayoutListener worked on Kotlin. Any idea?
I don’t see what’s wrong with the old way. Why do you people have to over engineer everything? Anyone reading your solution will take a while to figure out what you’re doing. This time wasted + the time it took you upfront to set it up is unlikely to outweigh the marginal benefits you might enjoy from using this pattern on an ongoing basis.
I can’t agree. Reducing the number of lines that do nothing makes the code much more readable. Functions like this were added to Android-KTX https://android.github.io/android-ktx/core-ktx/androidx.view/android.view.-view/index.html
In any case, this set of articles was meant to show the power of the language, and the kind of things you can achieve with it. The specific example is not that important.
It’s very fast implementation and anyone who used OnGlobalLayoutListener in their projects as myself will really appreciate this 🙂
Glad to know, thanks!
Shouldnt we remlve the GlobalLayoutListener in some lifecycle methods??? There can be a scenario where we have set the listener. but View is still not measured and the View’s hierarchy needs to be destroyed (may be Screen rotation). Your thoughts???
Great example, would like to suggest slight correction based on KTX source (since view tree observer references are mutable may create unintentional leaks)
“`
val vto = this.viewTreeObserver
vto.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
override fun onGlobalLayout() {
if (measuredWidth > 0 && measuredHeight > 0) {
f(this@postLayout)
when {
vto.isAlive -> vto.removeOnGlobalLayoutListener(this)
else -> viewTreeObserver.removeOnGlobalLayoutListener(this)
}
}
}
})
“`
default KTX source
recyclerView.doOnPreDraw {
// your action
}