icon-image
Welcome to Innominds Blog
Enjoy our insights and engage with us!

Application Building Simplified With Data Binding

By Pallavi Naramsetty,

Do you hate using findViewByIds? Ever felt if there is a better way to access UI XML elements? How about XML files handling simple logic by itself? The solution for it is data binding. It allows you to bind the UI elements in layouts with declarative format rather than programmatically.

Android Studio Supports Data Binding

Android Studio supports many editing features for data binding code. For example, it supports the following features for data binding expressions:

  • Syntax Highlighting
  • XML code completion
  • Flagging of expression language syntax errors
  • Navigate to declaration

If provided, the Preview pane in Layout Editor displays the default value of data binding expressions. For example, the Preview pane displays the my_default value on the TextView widget declared in the following example.

<TextView android:layout_width="match_parent"
	android:layout_height="wrap_content"
	android:text="@{student.name, default=default_value}"/>

If you want to display default values at the time of the design phase of your project, you need to add tools attributes instead of default values.

Pros of Data Binding

  • Obsoletes the use of findViewById
  • It separates the UI logic and source code
  • It provides a way to bind custom setters method or rename setter method of view’s attributes
  • Provides a way to bind event listener using lambda or method reference from XML

Enabling Data Binding in Android

To enable data binding, we need to add the following lines in app-level gradle file and sync the project.

build.gradle

android {
	....
	dataBinding{
		enabled true
	}
	...
}

Layout Integration

In a layout on the top of viewGroup (i.e. Constraint layout), add the <layout> tag followed by <data> and <variable> tag. Android studio provides automatic conversion by clicking alt+enter on root element. Later, you will get an option to convert to data binding layout and if you click enter, it automatically adds <layout> and <data> tags.

<layout 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">
	<data>
		<variable
		name="student"
		type="com.example.model.student" />
	</data>
	<androidx.constraintlayout.widget.ConstraintLayout>
	......
	</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Once data binding is integrated in layout file, go to Build -> Clean Project and Build -> Rebuild Project. This will generate necessary binding classes. If data binding classes are not generated, then go to File -> Invalidate Caches & Restart.

Binding Values

The student variable declared within data describes a property that may be used within this layout. Also, if you want to use expressions inside your layout, you can call attribute properties using the “@{}" syntax.

<TextView android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{student.getName}"/>

Layout variables are used to write layout expressions. Layout expressions are placed in the value of element attributes and they use the @{expression} format. Here are some examples.

android:text=@{student.getPercentage>=35?"pass":"fail"}

android:onClick="@{() -> callback.onClick(viewModel)}"

//for boolean and integer values

	android:checked="@{safeUnbox(fieldName)}"

//for integer values

android:text="@{String.valueOf(number)}"

Android data binding generates a Binding class based on the layout. This class holds all the bindings from the layout properties, i.e. the defined variable to the corresponding views. It also provides generated setters for your data elements from the layout. The name of the generated class is based on the name of the layout file. This name is converted to the Pascal case and the Binding suffix is added to it. For example, if the layout file is called activity_student_main.xml, the generated class is called ActivityStudentMainBinding. You can inflate the layout and connect your model via this class or the DataBindingUtil class. You can replace setContentView with the following code in the activity.


Student student = new Student("xyz",21)//your model class

ActivityStudentMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_student_main);

binding.setStudent(student); // generated setter based on the data in the layout file

You can use the inflate method on the generated class. This is useful for using data binding in fragments, ListView or RecyclerView.

ActivityStudentMainBinding binding=DataBindingUtil.inflate(layoutInflater, R.layout.activity_student_main, viewGroup, false);

binding.getRoot();

Student student = new Student("xyz",21)

binding.setStudent(student);

Data Binding in <include> Layouts

The <layout> tag is used in activity_student_main.xml layout to enable data binding. The <data> and <variable> tags are used to bind the student object. To pass the user to included content_student_main layout, bind:student=”@{student}” is used. Without this, the user object won’t be accessible in content_student_main layout.

activity_student_main:


<?xml version="1.0" encoding="utf-8"?>

<layout 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">

      <data>
          <variable
          name="student"
          type="com.example.model.Student" />
      </data>
<androidx.coordinatorlayout.widget.CoordinatorLayout> <include layout="@layout/content_student_main " bind:student="@{student}"/> </androidx.coordinatorlayout.widget.CoordinatorLayout> </layout>

In content_student_main again add <layout> tag to enable data binding. The <layout>, <data> and <variable> tags are necessary in both parent and included layouts.

content_student_main:

<?xml version="1.0" encoding="utf-8"?>

<layout 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">

  <data>
    <variable
    name="student"
    type="com.example.model.Student" />
  </data>

<LinearLayout

android:layout_width="match_parent"

android:layout_height="match_parent">

<TextView

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:text="@{student.name}" />

</LinearLayout>

</layout>

Event Handling

We can also bind the event similar to android:onClick can be assigned to a method in the activity.

android:onClick="percentage"

The former onClick attribute used an unsafe mechanism in which the percentage() method in the activity or fragment is called when the view is clicked. If that method, with the exact right signature doesn't exist, the app crashes.

android:onClick="@{() -> student.percentage()}"

The new way is much safer because it's checked at compile time and uses a lambda expression to call the percentage() method.

To bind an event, we use the same <variable> tag to handle the handlers.

HandlerClass

public class HandlerClass{

	public void onButtonClicked(){

		Toast.makeText(getApplicationContext,"Button is clicked",Toast.Length_Short);

	}
}    

To bind it in XML, use the following:

activity_main:

<layout xmlns:bind="http://schemas.android.com/apk/res/android">

<data>
	<variable
	name="handlers"
    type="com.example.student.HandlerClass" />
</data>

<android.support.design.widget.CoordinatorLayout ...>

<Button
   ...
  android:onClick="@{handlers::onButtonClicked}" />

</android.support.design.widget.CoordinatorLayout>
</layout>
  • To assign long press event, the method should return boolean type instead of void. public boolean onButtonLongPressed() handles the view long press
  • You can also pass params while binding. public void onButtonClickWithParam(View view, User user) receives the user object bind from UI layout. In the layout, the parameter can be passed using android:onClick=”@{(v) -> handlers.onButtonClickWithParam(v, user)}”.
  • To bind the events, binding.setHandlers(handlers) is called from the activity

MainActivity.java:

Student student = new Student("xyz",21);

ActivityStudentMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_student_main);

binding.setStudent(student);

HandlerClass handler=new HandlerClass ();

binding.setHandlers(handler);

Imports

We can import a class and use any of the method.

<data>
	<import type="com.example.Utils"/>
	<variable name="student" type="com.example.model.Student"/>
</data>

<TextView android:text="@{Utils.toUpper(student.name)}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

Using Observable Fields

Till now, we witnessed ways to bind a static value. If the value changes dynamically instead of changing UI explicitly, we use observable.

With data binding, when an observable value changes, the UI elements are bound to update automatically.

There are multiple ways to implement observability. Here are some of them:

  • Live Data
  • Observable Fields
  • Observable Classes

Why Data Binding to Live Data?

Advantages of using lifecycle aware components such as Live Data are:

  • It handles orientation changes during activity or when fragment receives the latest available data
  • No memory leaks as observers clean up after themselves when their associated lifecycle is destroyed
  • No crashes due to a stopped activity, as when an activity is in a back stack, it will not receive live data

To use a Live Data object with your binding class, you need to specify a lifecycle owner to define the scope of the Live Data object

ActivityStudentMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_student_main);binding.setLifecycleOwner(this);

So now we can use LiveData objects in the layout file as follows

<TextView android:text="@{viewmodel.text}" 
android:layout_width="wrap_content"
android:layout_height="wrap_content />

Binding Adapters to Create Custom Attributes

Binding Adapters are responsible for making the appropriate framework calls to set values. Binding Adapter annotated methods are defined to convert data from your data model for viewing. It is a public static method and perform data conversion and injection for specific custom attribute in layout file.

Binding Adapter for Existing Attributes

@BindingAdapter("android:text")

public static void setText(TextView view,CharSequence text){
	if(TextUtils.isEmpty(text)){
		view.setText("text is empty");
	}
	else{
		view.setText(text);
	}
}

Data binding works even if no attribute exists with the given name. You can create attributes for any setter by using data binding.

Create the binding method

@BindingAdapter("visible")

public static void setvisible(View view,boolean isVisible){

	view.setVisibility(isVisible? View.VISIBLE: View.GONE);

}

Add the custom attribute to your view

<ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" app:visible="@{isVisible}"/>

Binding Adapter With Multiple Parameters

Binding Adapter supports setting multiple attributes that must be placed as a list.

@BindingAdapter(value = {"imageUrl", "errorPlaceHolder"}, requireAll = false)

public static void loadImageWithError( ImageView imageView,String url, Drawable error) {

		if (url != null) {
			ImageLoader.load(imageView, url);
		} else if (error != null) {
			imageView.setImageDrawable(error);
		}
}
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:imageUrl="@{model.imageurl}"
app:errorPlaceHolder="@{@drawable/errorPlaceholder}"/>

Two-Way Data Binding

Two-way data binding is a technique of binding your objects to your XML layouts so that both the object can send data to the layout, and the layout can send data to the object. The @={} notation, which importantly includes the "=" sign, receives data changes to the property and listen to user updates at the same time.

<android.support.design.widget.TextInputLayout android:text="@={viewModel.name}" />

In order to receive data simultaneously, you have to implement Observable. Usually, BaseObservable and @Binding annotation are used.

public class BindableString extends BaseObservable {
	private String name;
	public String getName() {
	return name!= null ? value : “”;
}

public void setName(String name) {

	if (!Objects.equals(this.name, name)) {
		this.name= name;
		notifyChange();
	}

}

public boolean isEmpty() {
	return name== null || name.isEmpty();
}

Two-Way Data Binding Using Custom Attributes

If we want two-way data binding with custom attributes, we need to work with InverseBindingAdapter and InverseBindingMethod. The view that you will bind must have a change listener, such as the EditText that has the TextWatcher. Let's look at the below showcased samples.

  • Create an attribute that you want to bind and add it you xml,add = before the attribute.
    <EditText android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:full_name="=@{student.name}"/>
    

    In my case, android:full_name is custom attribute.

  • Create a Setter and Getter Method for Variable in Your Model
    public void setName(Cname) {
    	this.name= name;
    }
    
    public String getName() {
    	return this.name;
    }
  • Create a Binding Class with this Adapter
    You can create this class with any name and location within your project and it doesn't extend anything. The important part here is the annotation. In this class, there will be three methods.

    The first method will be the listener to the View

    @BindingAdapter(value = "textChanged")
    
    public static void setListener(EditText editText, final InverseBindingListener listener) {
    	if (listener != null) {
    		editText.addTextChangedListener(new TextWatcher() {
    		...
    		@Override
    			public void afterTextChanged(Editable editable) {
    				listener.onChange();
    			}
    		});
    	}
    }

    Here, the name of method could be anything but it is important to add the annotation @BindingAdapter(value = "textChanged"). Here, the name has a suffix changed. For example, if my attribute is colour, then my annotation will be colorChanged.

    The first parameter will be the view and the second parameter will be InverseBindingListener, and if the change occurs, it will call the third method, which we created.

    ** The second method will be the one, which sets input with the value coming from the model.**

    @BindingAdapter("text")
    
    public static void setText(EditText view, String value) {
    
    	if(!isValueSame(){
    		view.setText(value);
    	}
    
    }

    This @BindingAdapter("text") must have the name of the attribute we created before. It can have any name but I suggest to keep it as a setter. The first param is the view and the second one is the value that will be sent from the model. Here you will have to set the new value to the view and it is important to check if the new value is the same as the old one to avoid getting into a loop.

    The third method will be the getter method, which will call our model setAttribute

    @InverseBindingAdapter(attribute = "text")
    
    public static String getText(EditText editText) {
    
    		return editText.getValue.toUpperCase();
    
    }
    

    This method is annotated with @InverseBindingAdapter

    Every two-way binding generates a synthetic event attribute. This attribute has the same name as the base attribute, but it includes the suffix "AttrChanged". The synthetic event attribute allows the library to create a method annotated using @BindingAdapter to associate the event listener to the appropriate instance of view.

Conclusion

Data binding is a powerful feature and it simplifies the process of app building. It removes all the boilerplate code and avoids the use of reflection and generates binding classes at compile time for layouts.

Two-way data binding is base for MVVM architecture, as it is a great way of getting information about your view into your view models. If you’ve manually been binding listeners, then you should consider the built-in technique for two-way data binding provided by the @={variable} syntax.

About Innominds

Innominds is a leading Digital Transformation and Product Engineering company headquartered in San Jose, CA. It offers co-creation services to enterprises for building solutions utilising digital technologies focused on Devices, Apps, and Analytics. Innominds builds better outcomes securely for its clients through reliable advanced technologies like IoT, Blockchain, Big Data, Artificial Intelligence, DevOps and Enterprise Mobility among others. From idea to commercialisation, we strive to build convergent solutions that help our clients grow their business and realise their market vision.

To know more about our offerings, please write to marketing@innominds.com

References

https://developer.android.com/topic/libraries/data-binding

https://medium.com/@temidjoy/android-jetpack-empower-your-ui-with-android-data-binding-94a657cb6be1

https://www.vogella.com/tutorials/AndroidDatabinding/article.html

https://medium.com/@douglas.iacovelli/custom-two-way-databinding-made-easy-f8b17a4507d2

 

Topics: Mobility

Pallavi Naramsetty

Pallavi Naramsetty

Senior Engineer - Software Engineering

Subscribe to Email Updates

Authors

Show More

Recent Posts