View Binding or back to findViewById to replace Kotlin Synthetics?
Introduction
With kotlin synthetics being officially deprecated as part of Kotlin Android Extensions gradle plugin, and with Jetpack compose just around the corner, the Android team at Zattoo took the decision to avoid the migration to ViewBinding. Instead we chose a simple and reliable solution using findViewById but of course with some Kotlin sugar.
As of November ’20 Kotlin synthetic properties are officially deprecated. For anyone not familiar with them, they were part of the kotlin-android-extensions plugin and allowed you to access views in the layout XML, just as if they were properties of the fragment/activity/view using them. They were very straightforward and despite some drawbacks, I believe they were the most elegant way we ever had on Android to access the XML layout views, far better than the old default findViewById.
The reasons of the deprecation of the plugin are explained here, although another valid reason is that it’s not compatible with the new Kotlin backend. That means that you cannot (easily) use them together in a project with Jetpack Compose, (getting first stable version in July ’21), as the new way to write UI for Android.
So what can we do now for our views? The “official” recommendation is that we use the View Binding Jetpack library. A valid question though, arises:
Does it make sense to dedicate the time and effort to introduce to a big project a new library that is already known to be considered “old-way” (if not also deprecated) a few months down the road?
An alternative is to use Kotlin’s powers to reuse the old, reliable, default findViewById, hiding it behind the scenes with an elegant syntax, using extension methods and delegated properties. There is a very nice article here (definitely worth reading) which gives us two extension methods for activities and custom views, we can also create another one for RecyclerView ViewHolders:
And its use is super straightforward:
It seems like problem solved, we are defining our views as properties at the top of the class like we used to do back in the days, even with butterknife, and we are not using the ugly findViewById calls after setContentView.
Fragments
That’s nice, but what about fragments? In theory we could have an extension for them too, like the ones above. Well not so fast. Fragments unlike activities can have multiple view hierarchies created and destroyed by the system while keeping the same fragment class instance (navigating back and forth with navigation component or manually with adding fragments to backstack can easily expose this behavior).
This will cause memory leaks and/or bugs if we take the same approach as with activities and have a global property of a view lazily loaded. This property will hold a reference to the “old” view, keeping it after onDestroyView is called (memory leak) and won’t replace it with a new view after onCreateView is called again (bug) since it’s a val already lazily loaded. So in the above example, if it was used in a fragment instead of the activity, the button click handler would work only the first time the fragment view is loaded and will leak view reference. Not ideal at all.
Of course this is a common source for bugs in fragments you can easily make the same mistake with View Binding or with a RecyclerView adapter reference.
So how do we proceed? Well the best solution would be, to have this lazily loaded value reset to null when onDestroyView is called. And while experimenting for an implementation, I came across this blog post about fragments memory leaks mentioning the AutoClearValue helper from architecture components samples. Modifying it for views and combining it with an extension method from fragments we get this:
This can be used exactly the same way as with activities, without leaking any view reference!
Final Version
And this means we have a complete solution to replace Kotlin synthetics in any use case, without using “new” libraries, but with reusing the old findViewById in a more “Kotlin power” style:
I created a small sample app, that demonstrates this solution and the memory leaks caused by not clearing views references, using Leak Canary. Run the app and select baseline for non-leaky version and variation 1 for the leaks. Final step 4 in both versions contains a button to trigger a heap dump and check for memory leaks.
I hope this post will be helpful for your app’s migration from Kotlin synthetics, while waiting for the new Jetpack Compose era coming to Android by July 2021 with 1.0 stable!