When working in the UI layer of an Android app, it won't be long before we need to perform an operation upon one of our views. In order to do this, we first need to retrieve it via findViewById
. Although using the API can seem simple, it does introduce a block of boilerplate to our Activities. On top of this the code to bind all the views usually ends up in onCreate
, completely separate from the view properties themselves.
Now that Kotlin has entered the scene with new features we didn't previously have access to it seems like the perfect time to look for a new way of doing things. We will explore how some different approaches, including lazy initialisation and Kotlin Android Extensions, can be used to solve this problem.
The situation in Java
To set the scene and to demonstrate how Kotlin can help we first need to look at how these tasks are achieved in Java. To bind views to a field, we would use findViewById
, historically casting the resulting view to the correct subclass. For an activity, the views would commonly be bound within onCreate
, however, this may be elsewhere if views are retrieved at another time.
public class PlanningActivity extends AppCompatActivity {
private TextView planningText;
private ImageView appIcon;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_planning);
// Before: Needed to cast
planningText = (TextView) findViewById(R.id.planningText);
// Now: No longer need to
appIcon = findViewById(R.id.appIcon);
planningText.setText("Hello!");
}
}
The example here is very simple, containing only two views, whereas in reality many different views may need to be accessed. To avoid this repetitive boilerplate ButterKnife was created, which uses annotation processing to aid the binding of views.
public class PlanningActivity extends AppCompatActivity {
@BindView(R.id.planningText) TextView planningText;
@BindView(R.id.appIcon) ImageView appIcon;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_planning);
ButterKnife.bind(this);
planningText.setText("Hello!");
}
}
Lazy in Kotlin
The Kotlin programming language has many new features and so it feels natural that it may offer a better solution to what we were using before. A new concept in the language is that of delegated properties, specifically the lazy
delegated property. The way it works is that when the property is first accessed it will be initialized via the provided function and on future accesses the value will just be returned. This sounds perfect for binding views as the first access can call findViewById
and then subsequent calls can just provide the stored view.
private val planningText by lazy {
findViewById<TextView>(R.id.planningText)
}
A lazy delegated property is initialised on first access
To avoid typing all of this each time we want to bind a view, an extension function on Activity
can be created. We can also optimise performance slightly by disabling thread safety, due to the fact that the views will only be accessed from the main UI thread.
fun <ViewT : View> Activity.bindView(@IdRes idRes: Int): Lazy<ViewT> {
return lazy(LazyThreadSafetyMode.NONE) {
findViewById<ViewT>(idRes)
}
}
class PlanningActivity : AppCompatActivity() {
private val planningText by bindView<TextView>(R.id.planningText)
// or
private val planningText: TextView by bindView(R.id.planningText)
}
We now have a succinct and clear call site, with full control over the view binding and no generated code required. As bindView
is essentially a wrapper around findViewById
it can be applied elsewhere in the codebase, beyond just in Activity
.
Lazy elsewhere
Applying to views, such as in a RecyclerView.ViewHolder
is as simple as creating a similar extension function to give us access to the same API we had within Activities.
Where our technique encounters some issues is when used within a Fragment
due to differences in their lifecycle. Without alterations the lazy property can refer to an old view instance after the Fragment
view has been recreated. We can either avoid lazy view binding in Fragments or put together a solution using the Lifecycle Architecture Component. By creating our own version of Lazy
we can reset the stored value in onDestroyView
, causing the next access to call findViewById
again. An example implementation demonstrating this idea can be found within the sample code for the article.
Synthetic properties
As an alternative to implementing our own solution, especially if the situation with Fragments makes the lazy binding approach undesirable, we can use the Kotlin Android Extensions. The plugin automatically generates properties that match the IDs of the views in our layout file. Views no longer need to be stored manually as properties, as they are instead accessed via synthetic properties generated by the plugin.
Automatically generated properties that bind to our views
We first need to apply the plugin within our Gradle configuration.
build.gradle
apply plugin: 'kotlin-android-extensions'
build.gradle.kts
apply(plugin = "kotlin-android-extensions")
Once applied the plugin will generate synthetic properties for us to access views that are specified within our layout XML files, with names matching the IDs. To access the properties we need to add an import statement for each layout file. It is recommended to use the star syntax for the import, with Android Studio already having this specified within the default code style settings. The IDE should also add the import automatically when you try to reference one of the properties.
import kotlinx.android.synthetic.main.activity_planning.*
class PlanningActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_planning)
planningText.text = "Hello"
}
}
The plugin comes with other features such as applying to other custom containers and customising the view caching. Any features that are still considered experimental can be enabled within our Gradle configuration. Please find all the details of the other features on the plugin website.
androidExtensions {
experimental = true
}
Using Kotlin Android Extensions is a very clean approach, requiring practically no extra code. To previous users of ButterKnife it feels like an evolution to that approach. As with any potential solution it is best to try it out, see how it works and whether it fits with all the use cases a particular project has.
Wrap up
When this article was originally written I recommended the lazy property approach due to us being able to strike a great balance between brevity, avoidance of boilerplate and reducing reliance on generated code. Since this time I have found that my preferred solution has shifted towards the Kotlin Android Extensions plugin due to its simplicity and not having to worry if there will be issues with lazy binding in situations such as that of Fragments.
I hope the article was useful. If you have any feedback or questions please feel free to reach out.
Thanks for reading!
WRITTEN BY
Andrew Lord
A software developer and tech leader from the UK. Writing articles that focus on all aspects of Android and iOS development using Kotlin and Swift.
Want to read more?
Here are some other articles you may enjoy.