Kotlin synthetics is an excellent replacement for either findViewById or
ButterKnife when wiring view elements in XML layouts.
It reduces the lines of code, avoids annotations and provides narrower scoping.
Since it uses findViewById internally, it is just as performant as the other two techniques.
But there are some gotchas. You must be careful when using synthetics in Fragments since
they depend on the Fragment#onCreateView method having been
run. Ideally, use them in the Fragment#onViewCreated or any method called after that.
In Activities, you can reference synthetics anytime after
Activity#setContentView is called.
Nothing special must be done to existing layouts. Use them as they are.
<TextView
android:id="@id/myTextView"
android:layout_height="wrap_content"
android:layout_width="wrap_content" />Just reference the element with the same name as in the layout. There is no need to create an instance property.
myTextView.text = "Hello World"Include the Kotlin Extensions in your project. It will be included automatically when you create a new project with the Android Studion wizard. There is no need to add any additional libraries.
File: app/build.gradle
apply plugin: 'kotlin-android-extensions'
Download the sample project from
http://www.github.com/bizzguy/article-kotlin-synthetics
This sample app displays a screen with a counter starting at 0 and a button which will increment the counter when pressed.
- Kotlin language
- Kotlin extension library (this is something specific to android)
This has the gradle files for kotlin and Butterknife.
Project uses AndroidX
Synthetics requires no changes to layouts.
app/src/main/res/layout/main_fragment.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<TextView
android:id="@+id/countText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0"
android:textSize="96sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:text="Increment Counter"
android:textSize="24sp"
android:padding="32dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>Activity will create a fragment. The fragment will control the screen.
MainActivity.java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
getSupportActionBar().setTitle("Kotlin Synthetics");
if (savedInstanceState == null) {
getSupportFragmentManager().beginTransaction()
.replace(R.id.container, MainFragment.newInstance())
.commitNow();
}
}
}MainFragment.java
public class MainFragment extends Fragment {
public static MainFragment newInstance() {
return new MainFragment();
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.main_fragment, container, false);
return view;
}
}This step is just a quick reminder of how to connect XML layout elements to Java code. This just puts us all on the same page for those who might be new to Android and gives us a bit of a deep-dive into how findViewById actually works.
This will be imporant since all the binding techniques use findViewById under the hood.
TextView counterText;
Button button;
countText = view.findViewById(R.id.countText)
button = view.findViewById(R.id.button)
counterText.setText(Integer.toString(counter));
button.setOnClickListener(v -> {
counter++;
counterText.setText(Integer.toString(counter));
});
Let's drill down into the code for findViewById and see what is actually being executed.
The first call is to View.findViewById
public final <T extends View> T findViewById(@IdRes int id) {
if (id == NO_ID) {
return null;
}
return findViewTraversal(id);
}
The method 'View.findViewTraversal'
protected <T extends View> T findViewTraversal(@IdRes int id) {
if (id == mID) {
return (T) this;
}
return null;
}What is the protected modifier for? This is for methods with default
implementations that can be over-ridden in subtypes
Layouts have their own version of findViewTraversal
ViewGroup.findViewTraversal
@Override
protected <T extends View> T findViewTraversal(@IdRes int id) {
if (id == mID) {
return (T) this;
}
final View[] where = mChildren;
final int len = mChildrenCount;
for (int i = 0; i < len; i++) {
View v = where[i];
if ((v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) {
v = v.findViewById(id);
if (v != null) {
return (T) v;
}
}
}
return null;
}The important lines are
final View[] where = mChildren;
and
v = v.findViewById(id);
If the ViewGroup itself is not the view being looked for,
then search through all the children. The first view or child with the id is selected.
Although BK has other capabilities, such as wiring onClick listeners, its primary
purpose is to replace findViewById
The ButterKnife annotation @BindView tells BK which XML element
the property should be associated with.
@BindView(R.id.text_counter)
TextView counterText;
@BindView(R.id.button)
Button button;
@BindView fields must not be private or static
BK needs to be called to perform the binding. In this case, "binding" refers to the process of wiring the Java property to the XML layout element.
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.main_fragment, container, false);
ButterKnife.bind(this, view);
The @BindView annotations causes BK to create some hidden generated code.
BK names the binding file after the annotated class by using the class name appended with "_ViewBinding"
Now examine the file
Look in the directory
/app/build/generated/source/kapt/debug/com/article/kotlinsynthetics/b_java_butterknife
MainFragment_ViewBinding
Here's the constructor.
@UiThread
public MainFragment_ViewBinding(MainFragment target, View source) {
this.target = target;
target.counterText = Utils.findRequiredViewAsType(source, R.id.countText, "field 'counterText'", TextView.class);
target.button = Utils.findOptionalViewAsType(source, R.id.button, "field 'button'", Button.class);
}
There is a line for each property to be bound to a layout element. Notice the constructor does not return any views, it initializes the property in the original class.
We've got the reference to the property. BK now finds the reference to the view.
It does this in two steps.
First call findRequiredViewAsType which will return the reference as the
correct high level type
butterknife.internal\Utils
public static <T> T findRequiredViewAsType(View source, @IdRes int id, String who,
Class<T> cls) {
View view = findRequiredView(source, id, who);
return castView(view, id, who, cls);
}
Second by finding the view (as View type which must be recast)
This is the code to find the view.
public static View findRequiredView(View source, @IdRes int id, String who) {
View view = source.findViewById(id);
if (view != null) {
return view;
} + " (methods) annotation.");
...
}
Underneath it all, BK is just performing a findViewById
How is the binding initialized:
ButterKnife.bind(this, view)
public static Unbinder bind
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
constructor.newInstance(target, source);
Once ButterKnife#bind is run, the properties are initialized with references to the XML
view elements.
java.lang.reflect.Constructor uses reflection. Will this be a performance issue?
We'll convert the Java code to Kotlin just to demonstrate that any Java code (and any annotations) can also run in Kotlin. This is an option step - we could remove ButterKnife and convert to Kotlin in a single step.
Select Java file
Menu -> Code -> Convert Java File to Kotlin File
@BindView(R.id.countText)
internal var countText: TextView? = null
@BindView(R.id.button)
internal var button: Button? = null
Kotlin has assigned an internal modifier to the properties.
What is this for. Think of it as "module private". The properties are visible anywhere within the module.
Menu -> Tools -> Kotlin -> Show Kotlin Bytecode
Once the bytecode as been generated, select "Decompile" to turn the byte code into a readable form in Java (this is just a temporary file).
@BindView(-1000031)
@Nullable
private TextView countText;
@BindView(-1000090)
@Nullable
private Button button;
Remember, @BindView doesn't like privates so we get a compiler error.
Since we've already removed "private" what do we do?
Use the special Kotlin annotation @JvmField
@BindView(R.id.counterText)
@JvmField
var counterText: TextView? = null
@BindView(R.id.button)
@JvmField
var button: Button? = nullWhat does JvmField do?
From the doc:
Instructs the Kotlin compiler not to generate getters/setters for this property and expose it as a field.
Causes the field to have the same visibility as the underlying property.
See the Kotlin language documentation for more information.
Now let's replace ButterKnife with Kotlin Synthetics.
//@BindView(R.id.counterText)
//@JvmField
var counterText: TextView? = null
//@BindView(R.id.button)
//@JvmField
var button: Button? = null //ButterKnife.bind(this, view)
//var counterText: TextView? = null
//var button: Button? = null
Examine logcat to see the error
null pointer exception
fix by moving code to onCreateView
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
button!!.setOnClickListener {
counter++
countText!!.text = counter.toString()
}
}Re-run the app, it should work
See import statements
import kotlinx.android.synthetic.main.main_fragment.*Also notice the all the BK import statements are gone.
Show Kotlin Bytecode and Decompile back to java
public void onViewCreated(@NotNull View view, @Nullable Bundle savedInstanceState) {
Intrinsics.checkParameterIsNotNull(view, "view");
Button var10000 = (Button)this._$_findCachedViewById(id.button);
if (var10000 == null) {
Intrinsics.throwNpe();
}
var10000.setOnClickListener((OnClickListener)(new OnClickListener() {
public final void onClick(View it) {
MainFragment.this.counter = MainFragment.this.counter + 1;
TextView var10000 = (TextView)MainFragment.this._$_findCachedViewById(id.countText);
if (var10000 == null) {
Intrinsics.throwNpe();
}
var10000.setText((CharSequence)String.valueOf(MainFragment.this.counter));
}
}));
}Button var10000 = (Button)this._$_findCachedViewById(id.button);
Very strangely named method. Using understore or $ to begin a method name is a typical way of avoiding collisions with regular methods
_$_findCachedViewById
public View _$_findCachedViewById(int var1) {
...
View var2 = (View)this._$_findViewCache.get(var1);
if (var2 == null) {
...
var2 = var10000.findViewById(var1);
this._$_findViewCache.put(var1, var2);
}
return var2;
}
- If cache storage is null then create it
- Check cache storage for reference to view
- If not in cache then look up reference using
findViewById - If reference not in cache then add it to cache
private HashMap _$_findViewCache;
View var10000 = this.getView();
Fragment#getView
@Nullable
public View getView() {
return mView;
}
Fragment#performCreateView
mView = onCreateView(inflater, container, savedInstanceState);
Can't run Fragment#getView until after onCreateView is run.
Remove the call to super in onViewCreated
Remove the double bangs
Use the toString method on int (this is a Kotlin extension)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
countText.text = count.toString()
button.setOnClickListener {
count++
countText.text = count.toString()
}
}
And now we're done!
Kotlin Synthetics are an excellent solution for wiring XML layout elements to their controlling fragment or activity.
| Feature | Java findViewById | ButterKnife | Kotlin Synthetics |
|---|---|---|---|
| Scope | Broadest - can pick ids from any layout in app | same as FV | Narrower - can only pick ids from imported layouts |
| Number of Lines | Code for Property and wiring | Code for annotation, property and binding (most) | No code for wiring (least) |
| Performance | Uses findViewById | Uses findViewById - Also uses reflection to perform binding | Uses findViewById |
| Building | Minimal | Annotation processing | Minimal |
| Recommended by Jake Wharton | No | He wrote ButterKnife | He is now the Kotlin Ambassador at Google |
What happens if you name your view elements like this: android:id="@id/counter_text
Wiring is the technique of associated views defined in layouts with the Java or Kotlin code that controls them
Data binding goes one step further
- wiring
- assign data values and handle changes in data values from either the screen or other sources
counterText.setText(Integer.toString(counter));
counterText.setText(counter);
But one of the above fails at runtime (when executing)
counterText.text = counter
ButterKnife.bind(activity!!, view)
but this succeeds
ButterKnife.bind(this, view)
- findViewById - entire R.id namespace
- BK - entire R.id namespace
- KS - only imported layouts
Often the layout for an adapter can be moved into the adapter
AppCompatDelegateImpl.java#465
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mOriginalWindowCallback.onContentChanged();
}
- BK - all fields at start up
- KS - only fields that are actually used, when they are used name space
- FV - all layouts
- BK - only inflated view
- KS - only inflated view
Kotlin Annotation Processing Tool
Reach me at: [jamesharmon@gmail.com]
LinkedIn: [https://www.linkedin.com/in/jamesharmonandroid/]
Useful article about performance trade-offs
Kotlin properties are private by default (with getters/setters)
[https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html]
Read the Code!
[https://blog.codinghorror.com/learn-to-read-the-source-luke/]
Text-based Role Playing Game:

