Let Kotlin do the code for you — Part I: ButterKnife and Annotations

Learn How to implement simple versions of ButterKnife and MessageBus from scratch.

André Oriani
3 min readJan 21, 2023

Before View or Data Binding, retrieving a view in Android would always require the same boiler-plate code:

private TextView mField1;
private TextView mField2;
private Spinner mSpinner;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mField1 = (TextView) findViewById(R.id.textview1);
mField2 = (TextView) findViewById(R.id.textview2);
mSpinner = (Spinner) findViewById(R.id.spinner);
}

This article is part of a series. To see the other articles click here.

One of the first attempts to address the problem was Jake Wharton’s ButterKnife. It used annotations. Annotations allow the addition of metadata to program elements. With ButterKnife, the snippet above would morph into:

@BindView(R.id.textview1) TextView mField1;
@BindView(R.id.textview2) TextView mField2;
@BindView(R.id.spinner) Spinner mSpinner;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ButterKnife.bind(this)
}

Let’s do a simple re-implementation of the library in order to understand how it works. The first step is to create an annotation that links a view to an id.

@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
annotation class IdForView(@IdRes val id: Int)

The target is set to AnnotationTarget.Property so only view references that are properties fields in a class can be annotated. Retention is defined as AnnotationRetention.RUNTIME so the annotation is not discarded after compilation and it can be queried at runtime.

The next step is to implement the bind method. We are going to use reflection to find all the class fields that were annotated.

fun bind(activity: AppCompatActivity) {
activity::class.declaredMemberProperties.asSequence()
.filter { it is KMutableProperty1 && it.annotations.any { it is IdForView } }
.forEach {
val viewId = it.findAnnotation<IdForView>()?.id!! // It cannot be null due above check
val view = activity.findViewById<View>(viewId)
it as KMutableProperty1<AppCompatActivity, View>
it.isAccessible = true
it.set(activity, view)
}
}

declaredMemberProperties return all the properties for the activity that have been passed as an argument. Then we filter the list in order to retain only mutable properties (var ) that contains the annotation. For each property, we retrieve the value of the id field of the annotation, find the view for that id, and set the property to point to it. isAccessible set to true permits the modification of the value of a property even if it was declared with the private access modifier.

Here is the final code of our simple re-implementation of ButterKnife:

A similar technique was used to implement the so common publisher-subscriber EventBus libraries (here is one example), which would not only make it easy for different parts of your app to communicate, but they would also create some hard-to-debug memory leaks if they were not used with care. If you are not familiar with message buses they normally work this way. You annotate single-parameter methods of an object. The method’s argument is the “event” that will be exchanged between two parts of your app. Then you register the object in the bus. Whenever someone posts an event to the bus that matches the type of parameter of an annotated function of an object registered to the bus, that function will be called to receive the event.

Let’s see the code:

Lines 34–36 create the Subscribe runtime annotation whose targets are now functions.

The EventBus maintains a map from event types to pairs of registered objects and its subscriber functions (line 40).

The registermethod scans the received object for functions that have the annotation and it ensures that the function’s parameter list is unitary (lines 43–44). And then it adds the pair object — function to the register map.

Then the method post has little work to do. When it is called, it just needs to retrieve the list of the type of parameter that was passed. Then it traversals the list calling the functions with the argument that was passed and voilà!

Do not forget to check the other article of the series:

Let Kotlin do the code for you

3 stories

--

--

André Oriani

Brazilian living in the Silicon Valley, Tech Lead and Principal Mobile Software Engineer @WalmartLabs