Android View Binding

Ken Baldauf

Senior Software Engineer @ Originate

View Referencing

  • What is it?
    • Accessing XML views programmatically
  • How does it work?
    • Searches view hierarchy for a view whose android:id XML attribute matches a specified id

View Referencing Options

  • findViewById
  • ButterKnife
  • Data Binding
  • Synthetic Binding
  • View Binding

findViewById

  • Native, out of the box solution
  • Requires type casting
    • If compileSdkVersion < API 26 (Oreo)
  • Can return null if view with id is not found
  • Each view needs to be "found" independently
  • Can become cumbersome and/or ugly
  • Can be slow when view hierarchy is large

findViewById

// method signature
public <T extends View> T findViewById(int id);

// prior to api 26
public View findViewById(int id);
// java
TextView textView = (TextView) findViewById(R.id.my_text_view);
// prior to api 26
TextView textView = findViewById(R.id.my_text_view);
// kotlin
val textView: TextView = findViewById(R.id.my_text_view)
// prior to api 26
val textView: TextView = findViewById(R.id.my_text_view) as TextView

ButterKnife

  • Developed & designed by Jake Wharton
  • Annotation based
  • Behaves similarly to dependency injection 
  • Works with views, strings, dimens, drawables, etc.
  • Generates a view binding class at compile time
    • Replaces annotations with calls into binding class
  • Development is winding down
  • Android plugin 3.6.0-alpha10 breaks ButterKnife
    • ​All annotated ids map to 0 in library modules
  • Negative impact on build speed due to usage of annotation processor

ButterKnife

// plugin
apply plugin: 'com.jakewharton.butterknife'

// dependencies
api "com.jakewharton:butterknife:$butterknifeVersion"
annotationProcessor "com.jakewharton:butterknife-compiler:$butterknifeVersion"
@BindView(R.id.my_text_view) TextView textView;
@BindString(R.string.my_string) String string;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	setContentView(R.layout.my_layout);
	ButterKnife.bind(this);
}

Data Binding

  • Part of Android Jetpack & support library
  • Designed for setting data directly in xml layout
    • But can be used as a findViewById replacement
  • Type safe
  • Null safe
  • Generates binding class at compile time
    • Requires xml layout to be wrapped in special layout tag
    • Class name based off layout file name
  • Negative impact on build speed due to under the hood usage of annotation processor

Data Binding

// app/build.gradle
android {
    ...
    dataBinding.enabled = true
}
<layout>
   <androidx.constraintlayout.widget.ConstraintLayout
      android:layout_width="match_parent"
      android:layout_height="match_parent">
      <TextView
         android:id="@+id/my_text_view"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:text="@string/my_string" />
   </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
MyLayoutBinding binding = 
	DataBindingUtil.setContentView(this, R.layout.my_layout);
binding.my_text_view.setText("Hello OC Android");

Synthetic Binding

  • Part of the Android Kotlin Extension plugin
    • Built by JetBrains
  • Works only with Kotlin
  • Backed by findViewById + built-in view caching
  • Replaced at compile time with code generation
  • Cleaner & easy to read code
    • Ability to treat view ids directly as variables
  • Views are declared as platform types (TextView!)
    • Kotlin won't issue nullability error at compile time
    • Usage may fail at runtime though
  • Generated code could be costly
    • By default HashMap is used for cache
      • Primitive boxing needed to use int ids as map keys
  • Can easily be used incorrectly inside RecyclerViews
    • ​May result in findViewById called every time onBindViewHolder is called
  • Everything is put in global namespace
    • Android Studio allows you to import incorrect views
      • Can cause runtime errors
  • Typing isn't always guaranteed
    • May still need to typecast if same id is used for multiple view types 

Synthetic Binding

// app/build.gradle
apply plugin: 'kotlin-android-extensions'
import kotlinx.android.synthetic.main.activity_main.*

class MyActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        // Instead of findViewById<TextView>(R.id.textView)
        textView.setText("Hello, world!")
    }
}
public View _$_findCachedViewById(int var1) {
   if (this._$_findViewCache == null) {
      this._$_findViewCache = new HashMap();
   }

   View var2 = (View)this._$_findViewCache.get(var1);
   if (var2 == null) {
      var2 = this.findViewById(var1);
      this._$_findViewCache.put(var1, var2);
   }

   return var2;
}

View Binding

  • Added in Android Studio & plugin 3.6
    • Currently in beta
  • Type safe
  • Null safe
    • Prevents referencing inaccessible view ids
    • Will mark views as nullable if only selectively available
  • Generates binding class for all layouts at compile time
    • No need to modify xml files like Data Binding
  • Can add tools:viewBindingIgnore to views you don't want in binding class
  • More efficient build times than Data & Synthetic Binding
    • Designed in part to address Data Binding build times

View Binding

// app/build.gradle
android {
    ...
    viewBinding.enable = true
}
// can also pass ViewGroup parent, boolean attachToParent to inflate
MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflator)

// if in activity
setContentView(binding.root)

binding.my_text_view.text = "Hello OC Android"
public final class FragmentSecondBinding 
	implements ViewBinding {
  @NonNull
  private final ConstraintLayout rootView;

  @NonNull
  public final TextView myTextView;

  private FragmentSecondBinding(
  	@NonNull ConstraintLayout rootView, 
  	@NonNull TextView myTextView) {
    this.rootView = rootView;
    this.myTextView = myTextView;
  }

  @Override
  @NonNull
  public ConstraintLayout getRoot() {
    return rootView;
  }

  @NonNull
  public static MyLayoutBinding inflate(@
  	NonNull LayoutInflater inflater) {
    return inflate(inflater, null, false);
  }
  @NonNull
  public static MyLayoutBinding inflate(
  	@NonNull LayoutInflater inflater,
        @Nullable ViewGroup parent,
      	boolean attachToParent) {
    View root = inflater.inflate(
    	R.layout.my_layout, parent, false);
    if (attachToParent) {
      parent.addView(root);
    }
    return bind(root);
  }  

  @NonNull
  public static MyLayoutBinding bind(
  @NonNull View rootView) {
    // The body of this method is generated 
    // in a way you would not otherwise write.
    // This is done to optimize the compiled 
    // bytecode for size and performance.
    String missingId;
    missingId: {
      TextView myTextView = rootView.findViewById(
      	R.id.my_text_view);
      if (myTextView == null) {
        missingId = "myTextView";
        break missingId;
      }
      return new MyLayoutBinding(
      	(ConstraintLayout) rootView, myTextView);
    }
    throw new NullPointerException(
    	"Missing required view with ID: "
        .concat(missingId));
  }

Questions?

Android View Binding

By Kenneth Baldauf

Android View Binding

A look into Android's new View Binding and a comparison with its competitors.

  • 490