Optimize views, listeners, resources binding using ButterKnife in Android Android 29.12.2016

android_butterknife.png In this article, I will introduce an injection library for Android development that can help create more beautiful code. Android ButterKnife is an open source view injection library for Android created by Jake Wharton.

In every Android application, you have to use the findViewById() method for each view in the layout that you want to use in your application's code. But as applications' designs get more complex layouts, the call to this method becomes repetitive and this is where the ButterKnife library comes in.

ButterKnife is a view binding tool that uses annotations to generate boilerplate code for us. Something like this:

@BindView(R.id.tvUsername) TextView tvUsername;
@BindView(R.id.ivAvatar) ImageView ivAvatar;
@BindView(R.id.btnLogin) Button btnLogin;

ButterKnife allows developers to perform injection on arbitrary objects, views and OnClickListeners so they can focus on writing useful code. Consider ButterKnife as a reduction library. It replaces findViewById() with @BindView() and setFOOListener() calls with @onClick() making code cleaner and more understandable. ButterKnife enables focus on logic instead of glue code and reduces development time by reducing redundant coding.

ButterKnife uses compile time annotations and so not generating any overhead at run-time. Instead of slow reflection or generating views at run-time, it actually creates required code ahead of time. So it doesn't cause any performance issues or slow down your application.

Before getting started with ButterKnife, you need to configure your Android project. To use ButterKnife in our application we need to add the following dependency to our build.gradle.

compile 'com.jakewharton:butterknife:8.4.0'

In the `onCreate()`` method of the activity before using any views we need to add this line of code.

ButterKnife.bind(this);

When using fragments we need to specify the source of the view in the onCreateView() as:

View view = inflater.inflate(R.layout.profile_fragment, null);
ButterKnife.bind(this, view);

Now that you have configured Butter Knife its time to move on to the main features this injection library can provide:

  • Binding views and resources
  • Event listeners
  • Listview adapters, RecyclerView implementation

A simple activity example with ButterKnife is shown below:

public class MainActivity extends Activity {  
    @BindView(R.id.tvUsername) TextView tvUsername;
    @BindView(R.id.btnLogin) Button btnLogin;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);

        tvUsername.setText("Hello!");

        @OnClick(R.id.btnLogin) 
        void buttonClick(View v) {  
            // action
        }
    }
}

In the above code snippet @OnClick is the ButterKnife annotation that removes the need for the setOnClickListener method. The method below is automatically configured to that annotation. An argument inside the method is optional.

We can specify multiple IDs in a single binding for common event handling as shown below:

@OnClick({ R.id.btn1, R.id.btn2, R.id.btn3 })
public void switchStatus(Button button) {
    switch(button.getId()) {
        case R.id.btn1:
            Toast.makeText(this, "Status 1!", LENGTH_SHORT).show();
            break;
        case R.id.btn2:
            Toast.makeText(this, "Status 2!", LENGTH_SHORT).show();
            break;
        case R.id.btn3:
            Toast.makeText(this, "Status 3!", LENGTH_SHORT).show();
            break;
    }
}

In the above code a specific type of view (Button) is automatically casted.

It will throw exception if the target view can not be found. To indicate that the field may not be present in the layout, use any variant of the @Nullable annotation.

@Nullable  
@BindView(R.id.edUsername) 
EditText edUsername;

More listeners attachment

@OnItemClick(R.id.list_of_things) 
void onItemClick(int position) {
    Toast.makeText(this, "You clicked: " + adapter.getItem(position), LENGTH_SHORT).show();
}

@OnItemSelected({R.id.spinnerCountry}) 
void onItemSelected(Spinner spinner, int position) { 
    // ...
} 

@OnTextChanged(value = R.id.edUsername, callback = OnTextChanged.Callback.AFTER_TEXT_CHANGED)
void usernameChanged(Editable editable) {
    validateUsername(editable);
}

Here @OnTextChanged annotation uses a callback attribute to specify the callback method to which the annotated method has to be bound. There are callback attributes: TEXT_CHANGED, BEFORE_TEXT_CHANGED, AFTER_TEXT_CHANGED.

Also you can use ButterKnifeZelezny that allows one-click creation of ButterKnife view injections.

Fragments

An example of implementing ButterKnife in fragments is given below:

public class SomeFragment extends Fragment {
    @BindView(R.id.textView) 
    TextView textView;
    private Unbinder unbinder;

    @Override
    public View onCreateView(LayoutInflater inflater,
            ViewGroup container, Bundle savedInstanceState) {

        ViewGroup rootView = (ViewGroup) inflater
                .inflate(R.layout.some_layout, container, false);

        unbinder = ButterKnife.bind(this, rootView);

        textView.setVisibility(View.VISIBLE);
        return rootView;
    }

    @OnClick({ R.id.imageView, R.id.textView })
    public void doSomething() {
        // do something when imageView or textView is clicked.
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        unbinder.unbind();
    }
}

Bind resources

This is one of the good feature ButterKnife provides. Same as we can eliminate findViewById() by using @Bind* annotation. It also allows us to bind colors, dimens, string, arrays, drawable, etc. resources.

@BindColor(R.color.red) int red; 
@BindString(R.string.activity_title) String activityTitle;
@BindDimen(R.dimen.btn_horizontal_margin_common) Float btnHorizontalMarginCommon;
@BindDrawable(R.drawable.ic_instructions) Drawable iconInstructions;

@BindArray(R.array.success_titles) String[] successTitles;
@BindArray(R.array.powers_of_2) int[] powersOfTwo;

The following resource types are available: BindArray, BindBitmap, BindBool, BindColor, BindDimen, BindDrawable, BindInt, BindString.

Group multiple views into a List or Array

@BindViews({ R.id.first_name, R.id.middle_name, R.id.last_name })
List<EditText> views;

The apply() method allows you to act on all the views in a list at once.

ButterKnife.apply(nameViews, DISABLE);
ButterKnife.apply(nameViews, ENABLED, false);

An Android Property can also be used with the apply method.

ButterKnife.apply(nameViews, View.ALPHA, 0.0f);

We can use Action to change a property of view list and use Setters to pass external values when performing operations on a view list.

RecyclerView

The process of building a RecyclerView is long and complex, so I recommend you first read my article explaining how.

I will focus on the parts of the RecyclerView related to ButterKnife, the ViewHolder and view binding.

The ViewHolder example of a RecyclerView looks like this.

public class MovieViewHolder extends RecyclerView.ViewHolder {
    @BindView(R.id.cardView)
    CardView cv;
    @BindView(R.id.textView)
    TextView title;
    @BindView(R.id.imageView)
    ImageView imageView;

    View_Holder(View itemView) {
        super(itemView);
        ButterKnife.bind(this, itemView);
    }
}

This ViewHolder class represents one row of the RecyclerView with the following layout.

<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/cardView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="@dimen/activity_vertical_margin"
    app:cardCornerRadius="8dp"
    app:cardElevation="8dp">

    <ImageView android:id="@+id/imageView"
        android:layout_width="120dp"
        android:layout_height="110dp"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:src="@drawable/butterknife"/>

    <TextView android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center|right"
        android:layout_marginRight="40dp"
        android:text="Large Text"
        android:textAppearance="?android:attr/textAppearanceLarge"/>
</android.support.v7.widget.CardView>

In an Activity class bind the RecyclerView and provide data to its Adapter.

@BindView(R.id.recyclerview)
RecyclerView recyclerView;

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

    //Setup the RecyclerView
    List<Movie> movies = new ArrayList<>();
    for (int i = 0; i < 7; i++) {
        movies.add(new Movie("Movie " + 1, R.drawable.poster));
    }

    MoviesAdapter adapter = new MoviesAdapter(movies, getApplication());
    recyclerView.setAdapter(adapter);
    recyclerView.setLayoutManager(new LinearLayoutManager(this));
}

How to use ButterKnife with ListView you can read here.

Useful links