Add View Binding to Replace findViewById

View Binding initializes every UI element which has an id in the layout files and exposes them to the developers through a generated class.

If you think writing findViewById in every activity or fragment should not be a developer's job then you are reading the right article.

Does the code inside the activity or fragment start like this?

Manual UI elements initialization

// Initialize the UI elements
final ConstraintLayout constraintLayout = findViewById(R.id.coordinatorLayout);
final TextView responseTextView = findViewById(R.id.responseTextView);
final BottomAppBar bottomAppBar = findViewById(R.id.bottomAppBar);
final FloatingActionButton fab = findViewById(R.id.fab);
final RecyclerView productRecyclerView = findViewById(R.id.productRecyclerView);
final Button filterButton = findViewById(R.id.filterButton);
final Button sortButton = findViewById(R.id.sortButton);

Butter Knife UI elements initialization

// UI elements
@BindView(R.id.coordinatorLayout) CoordinatorLayout coordinatorLayout;
@BindView(R.id.responseTextView) TextView responseTextView;
@BindView(R.id.bottomAppBar) BottomAppBar bottomAppBar;
@BindView(R.id.fab) FloatingActionButton fab;
@BindView(R.id.productRecyclerView) RecyclerView productRecyclerView;
@BindView(R.id.filterButton) Button filterButton;
@BindView(R.id.sortButton) Button sortButton;

Doesn't this code look ugly? Even the article looks ugly by adding it to the top of the page. Without additional dependencies, Android now provides in-built support to replace findViewById in both Java and Kotlin.

Enable View Binding for the module by adding the following lines to the android block inside the module level build.gradle file.

Between Android Studio versions 3.6 and 4.0

viewBinding {
    enabled = true
}

From Android Studio version 4.0

buildFeatures {
    viewBinding true
}

Now, for an XML file with a name activity_main, a corresponding view binding class with the name ActivityMainBinding file would be generated.

The generated ActivityMainBinding class implements the ViewBinding interface to override getRoot method. The getRoot method returns the root element of the layout.

Inside the MainActivity or the activity class that is setting its content using the layout named activity_main, Our typical,

// Set the content view
setContentView(R.layout.activity_main);

changes to

// Inflate the layout
final ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
// Set the content view
setContentView(binding.getRoot());

Calling inflate method inflates the activity_main layout and internally calls bind method where it initializes the views available in the layout.

@NonNull
public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater) {
  return inflate(inflater, null, false);
}

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

@NonNull
public static ActivityMainBinding 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.
  int id;
  missingId: {
    id = R.id.activityContent;
    View activityContent = rootView.findViewById(id);
    if (activityContent == null) {
      break missingId;
    }
    ActivityContentBinding activityContentBinding = ActivityContentBinding.bind(activityContent);

    id = R.id.bottomAppBar;
    BottomAppBar bottomAppBar = rootView.findViewById(id);
    if (bottomAppBar == null) {
      break missingId;
    }

    CoordinatorLayout coordinatorLayout = (CoordinatorLayout) rootView;

    id = R.id.fab;
    FloatingActionButton fab = rootView.findViewById(id);
    if (fab == null) {
      break missingId;
    }
    return new ActivityMainBinding((CoordinatorLayout) rootView, activityContentBinding,
        bottomAppBar, coordinatorLayout, fab);
  }
  String missingId = rootView.getResources().getResourceName(id);
  throw new NullPointerException("Missing required view with ID: ".concat(missingId));
}

If you observe the generated class, inflate method is overloaded twice. One takes LayoutInflater and the other one takes additional parameters ViewGroup, attachToParent.

Does the second overloaded method look familiar? It is similar to inflating layout inside a fragment. This is the method, we use to perform View Binding inside a Fragment. Currently inside onCreateView,

return inflater.inflate(R.layout.main_fragment, container, false)

changes to

binding = MainFragmentBinding.inflate(inflater, container, false);
return binding.getRoot();

The only pain point with View Binding in fragments is that we need to set the binding variable to null when the fragment is destroyed.

@Override
public void onDestroyView() {
    super.onDestroyView();
    binding = null;
}

The binding variable holds references to all the views in the layout. The generated ViewBinding class exposes the variables by setting them to public.

@NonNull
private final CoordinatorLayout rootView;

@NonNull
public final ActivityContentBinding activityContent;

@NonNull
public final BottomAppBar bottomAppBar;

@NonNull
public final CoordinatorLayout coordinatorLayout;

@NonNull
public final FloatingActionButton fab;

For example, we can set listeners to the fab button which has an id fab as shown below.

Without View Binding:

// Initialize the UI element
final FloatingActionButton fab = findViewById(R.id.fab);
// Set a click listener
fab.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        // Show a snack message
        showSnack(getString(R.string.clicked_button_fab));
    }
});

With View Binding:

// Set a click listener
binding.fab.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        // Show a snack message
        showSnack(getString(R.string.clicked_button_fab));
    }
});

View Binding provides null safety and type safety. The fab button returned by the binding variable is of type FloatingActionButton. As soon as an id is removed, the compiler shows an error that the symbol cannot be resolved and there is no need to build the project to see which keys are removed.

In case, we have include tags that add nested layouts then simply add an id to the include tag to reference the nested elements in the layout.

<include android:id="@+id/activityContent" 
    layout="@layout/activity_content"/>

This gives us access to the views available inside activity_content like responseTextView, productRecyclerView, filterButton, sortButton and we can update the text view like below.

binding.activityContent.responseTextView.setText(response);

Unlike other layout files, if we want to skip generating ViewBinding classes for some, we can always add tools:viewBindingIgnore="true" to the top view group.

The code becomes cleaner by removing the manual or butter knife initialization of UI elements in the activity or fragment. Kotin Android Extensions provide similar feature but the solution is limited to pure Kotlin Android projects.

View Binding is similar to Data Binding but doesn't provide any advanced features like binding views in the layout or two-way binding.

Popular posts from this blog

How to Read Metadata from AndriodManifest File

Create Assets Folder, Add Files and Read Data From It

Add Options Menu to Activity and Fragment

How to Change Material Chip Text Size, Text Style and Font

Add Spacing to Recycler View Linear Layout Manager Using Item Decoration