View Binding: farewell to Kotlin Syntetic

Kotlin Synthetic was one of the coolest features when I started playing with Kotlin. In the Android Java world, to get on hold of a view, you needed to use findViewById() and then cast to the appropriate view type. This was one of the most repetitive and boring tasks when creating a new view/screen.

Then Kotlin and its Synthetics came around. If you had a view with id hello, you would write hello and  you could access it. No boilerplate, no findViewById, not a thousand member variables in your class.

But it turns out that the concept was kind of too relaxed. They had a global namespace and there were no nullability checks. This meant that you were able to access hello2 on screen 2, even though you were on screen 1, leading to inevitable crashes.

So a step back is probably the right balance. Kotlin Android extensions (which Kotlin Synthetic is part of) are deprecated, and the recommended way to replace them is the View Bindings. This has a bit more boilerplate but offers type and nullability safety.

Set up

In the Android section of your build.gradle add this:

android {
    [...]
    buildFeatures {
        viewBinding true
    }
}

Auto-generation

Now when you do Build -> Make project, for every XML file you have a corresponding binding class will be generated. For instance, for your fragment_screen_1.xml, the FragmentScreen1Binding will be created. These classes contain a member variable for every child view in the XML layout.

Tip: You might need to do File -> Sync Project with Gradle files to get Android Studio to recognize those generated classes.

Create the bindings

In Fragments

private var binding: FragmentScreen1Binding? = null // 2.

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    binding = FragmentScreen1Binding.inflate(inflater, container, false) // 1.
    return binding.root // 3.
}

override fun onDestroyView() {
    super.onDestroyView()
    binding = null // 4.
}
  1. Instead of the regular inflate() you will use the auto-generated class to inflate your view.
  2. This binding is non-null only between onCreateView() and onDestroyView().
  3. The root view of your fragment_screen_1.xml layout.
  4. Note that Fragments outlive their views. Clean up binding after destroying the inflated view for avoiding any memory leaks.

In Activities

private lateinit var binding: FragmentScreen1Binding

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = FragmentScreen1Binding.inflate(layoutInflater)
    setContentView(binding.root)
}

Similar to the fragment without the need for clean up since the inflated view is tightly associated with the activity lifecycle.

Use the bindings

For every child view in the XML layout, there's a member variable with the same name in camel case in your binding variable. The views without an ID are not accessible.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <TextView
    	android:id="@+id/hello_world"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
        
</LinearLayout>
fragment_screen_1.xml
binding.helloWorld.text = "Hello world!"
FragmentScreen1.kt

In case you were using Kotlin Synthetic, remove the imports for kotlinx.android.synthetic.* and replace them with the data bindings equivalents.

For further reading, check out the official doc (and Kotlin Synthetic migration guide). If you don't enjoy Android Views, be a little more patient until Jetpack Compose becomes stable :) Until then, happy coding!