Click here to Skip to main content
15,867,832 members
Articles / Mobile Apps / Android

Scalable Data Binding Framework for Android

Rate me:
Please Sign up or sign in to vote.
3.67/5 (4 votes)
28 Mar 2016CPOL9 min read 11.8K   2  
Describes a scalable data binding implementation for Android

Introduction

Data binding is considered one of the most desirable functionalities in any framework for developing business applications. It generally allows the programmer to bind data with UI elements through declarative expressions. This goodness saves lots of time in writing tedious code. In a previous article, some aspects were covered about Enterlib, a Model-View-ViewModel framework for Android. That article introduced some examples of its data binding capabilities and binding expressions. Therefore, I suggest to take a look at the article A MVVM framework for Android - Enterlib in order to get an overview of the Framework before reading this article.

Background

This section will cover a short description about core components of the data binding infrastructure of Enterlib and that will be constantly referred throughout the article.

At the core of this data binding implementation is found the Field class, this class was designed to extend the properties of an UI element like the View class. In addition, it's in charge for supporting data binding, validations amount other functionalities at the view level. So it wraps the view hierarchy into a field hierarchy for those views with binding expressions assigned. At the time of writing this article, validations are only supported for a special property of Field named Value the main reason for that is it wasn't relevant to be supported for all View's properties at that moment. The framework defines several concrete classes extending Field that redefine the Value property for its corresponding views. Furthermore, the developer may register its own FieldFactory in order to provide the required Field for a custom View being processed. However, if there are no FieldFactory for providing a Field for a given View, a GenericField will be created and linked to the View by default. The GenericField enables all the data binding capabilities, but the Value property will always return null, so data binding involving this property will be useless.

The second important class is the Form. Use this class for accessing the binding framework and the fields hierarchy. Also, the framework defines a FormFragment that leverage the Form's management and inserts into proper points of the Fragment life cycle the Form instantiation and states saving. If it's desired to employ the MVVM architecture, then you can inherit from BindableFragment or one of its descendants like BindableEditFragment, BindableDialogFragment, BindableEditFragment or ListFragment.

Java
public class Form {

	/**Interface for providing a Field for a custom View*/
	public static interface FieldFactory {
		Field createField(Class<?> viewClass, View view);
	}

	/**Register a user {@link FieldFactory}. It's recommend to use
	 * this method in the Application's onCreate method*/
	public static void addFactories(FieldFactory... factories);

	/**Update the target properties
	 * Use the viewModel as the sourceObject*/
	public void updateTargets();


	/**Update the target properties
	 * @param sourceObject The source object of the binding hierarchy */
	public void updateTargets(Object sourceObject);

		/**Updates the source properties
	 * Use the viewModel as the sourceObject  of the binding hierarchy*/
	public void updateSource() ;

	/**Updates the source properties
	 * @param sourceObject The source object */
	public void updateSource(Object sourceObject);

	/**Set the field error messages
	 * @param ei Contains a ValidationResult collection where
	 * the ValidationResult.getField() returns the name of the invalid source property */
	public void setFieldErrors(ErrorInfo ei);

	/** Restore the field's value and states from the savedInstanceState */
	public void restoreState(Bundle savedInstanceState);

	/** Save the field's value and states in outstate*/
	public void saveState(Bundle outState);

	/**Link a view hierarchy to its corresponding
	 * fields. Use this method when
	 * the view hierarchy is destroyed and recreated and
	 * you want to maintain the fields states */
	public void bindView(View view);

	/** Creates the Form from the view hierarchy
	 * @param bindingResources A dictionary like object
	 *                         containing references for the bindings
	 * @param rootView The root of the view hierarchy
	 * @param viewModel The default source object for the bindings
	 * */
	public static Form build(BindingResources bindingResources, View rootView,
			Object viewModel);

	/**Returns true if all the fields, and other IValidator objects
	 * are valid.*/
	public boolean validate();
}

As seen earlier, the Form is created with Form.build(…) this will process the view hierarchy and creates the respective Fields. You can pass as a parameter the viewModel and optionally a BindingResources that can provide additional information for the binding framework such as IValueConverters, IValueValidators amount others instances.

Using the Form, you can register IValidator instances to do validation logic involving several fields. For example, it may be required to have a date that must be before another or a field’s value must match another field’s value. All the previous validations are done at the view layer, but on the other hand, some validations must be done at the business layer. Therefore, the framework provides a way of routing those validation messages back to the UI by means of throwing a ValidationException from the business layer. The ValidationException contains an ErrorInfo that can be passed to the Form with Form.setFieldErrors for reflecting those validation messages in the UI.

Properties as commonly used in the Java beans terminology are any public instance member or public instance method with "get" or "is" as prefix and zero arguments. Or any public instance method with "set" as prefix and one argument only. A property may have getter and setter, examples of properties are Value with getValue() and setValue(Object), Enabled with isEnabled(), setEnabled(bool), Visibility with getVisibility(int), setVisibility(int), etc.

Other concepts are:

  • BindingProperty: A binding property is an extension of the property concept. It is used for initializing its corresponding binding expression member or specifying some behavior for the target property or the full Field. A BindingProperty must be defined as static members of a Field class and they are inherited for its descendant classes. For example, the Field class defines the binding properties Value, Required, Value, Restorable, Converter amount others. This allows a scalable binding mechanism the user can extend by defining new Field classes with the necessary binding properties.
  • Target property: The target property is related to the UI element and can be a BindingProperty or common property declared in a Field or its linked View.
  • Source property: The source property is any property of the source object.
  • Target Object: The target is the object that declares the target properties like the Field or its linked View.
  • Source Object: The source object declares the source properties. It can be the ViewModel when creating the Form, the object in Form.updateTargets(Object), Form.updateSource(Object) or any object accessible from the ViewModel.

Here are some samples of binding properties defined in the Field class. The set method is invoked with the binding expression member represented by a ExpressionMember after the binding expression is parsed. Also, the Field implements IPropertyChangedListener so it can be notified to update a target property when its binded source property changed its value.

Java
public abstract class Field extends DependencyObject
	implements IValidator, IPropertyChangedListener {

	public static final BindingProperty<Field> ValueProperty = registerProperty(Field.class,
			new BindingProperty<Field>("Value") {
			@Override
			public void set(Field object, ExpressionMember value,
							BindingResources dc) {
                        //Performs some initialization for the Value property like
                        //storing the binding source property name for validation and state
                        //saving purposes if they are enabled
                        object.valueBinding = value.getValueString();
            }
	});
	public static final BindingProperty<Field> RequiredProperty = registerProperty(Field.class,
			new BindingProperty<Field>("Required") {
			@Override
			public void set(Field object, ExpressionMember value,
						BindingResources dc) {

					//Set the Value property required
					object.setRequired(value.isValueTrue());
		}
	});

	//Defines a command property
	public static final BindingProperty<Field> ClickCommandProperty = registerCommand(Field.class,
			"ClickCommand");
}

Binding Expressions Examples

In the example below, the object returned from the ViewModel's Person property is binded to the parent LinearLayout, the object's Name property is binded to the Value property of the EditText. In fact, neither the LinearLayout nor the EditText define the Value property, but the framework knows it's the property of the related Field. The EditText also defines that its value is required, in that case, the BindingProperty "Required" was used. Also, the binding expression may use properties not defined in a Field but defined in its View like Visibility or Enabled.

XML
<LinearLayout android:layout_width="match_parent"
			android:layout_height="wrap_content"
			android:orientation="vertical"
			android:tag="{Value:Person}" >

	<EditText
			android:layout_width="match_parent"
			android:layout_height="wrap_content"
			android:tag="{Value:Name, Required:true}" />

</LinearLayout> 

The Binding Expressions Grammar

The grammar of the binding expressions are shown below:

Java
BindingExpression = { ID : RValue (, ID : RValue )* }
RValue = ID | BindingExpression | ArrayExpression
ArrayExpresion= [ RValue (, RValue)* ] 

ID = the target property name
The means of symbols used in the grammar definitions are:

  • () for grouping elements
  • | specify several options
  • * specify zero or more elements

The tokens are { } : [ ] ,. The last token is used for separating the binding expression members.

A more complex example:

XML
<Spinner android:id="@+id/spCategories"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:tag="{Value:categoryId,
                          Items:Categories,
                          Comparer:CategoryComparer,
                          Converter:CategoryConverter,
                          Required:true,
                          ItemTemplate:template_category,
                          Visibility:{Source:CanSelectCategory,
                                      Converter:BoolToVisibility}}" /> 

In the example below, the framework instantiates a SpinnerField for the Spinner. Some target properties referenced in the binding expression are described below:

  • Items: A BindingProperty defined in the ItemsField base class of SpinnerField. Use it in order to retrieve the elements shown in the Spinner’s dropdown list, a ListView or any ViewGroup.
  • Comparer: Is a BindingProperty for SelectionField like the SpinnerField. Can be used for setting the selected position of the Spinner. It will compare the objects in Items with the object of Value. The reference for Comparer can be resolved from the BindingResource or the ViewModel if it's not found in the first one.
  • Converter: Is a BindingProperty defined in Field. Use it for set an IValueConverted between the target and source properties. In the example above, the target property "Value" returns a Category object but the source property expects an integer, so the Converter will get the id from the Category. The reference for Converter can be resolved from the BindingResource or the ViewModel if it's not found in the first one.
  • ItemTemplate: A BindingProperty defined in the ItemsField base class. Use it for specifying a custom layout for displaying the Items.
  • Visibility: A normal property defined in the View class. Optionally, you can set an IValueConverter using the Converter keyword for converting a Boolean from the source property to an Integer of the target property. Note in this case, the source property is binded using the Source keyword.

Using the Code

The following example will show how to use data binding. For simplicity, it will be used without the MVVM infrastructure but it can be integrated nicely as you saw before. The example will cover the development of a Movie Center app for renting films. So let's start defining our business models and contracts.

The Movie Center Business Model

This interface defines the contract for setting an image and the Film and Actor model implementation.

Java
package com.moviecenter.models;

import android.graphics.drawable.Drawable;

public interface OnImageLoadedListener {
	void setImage(Drawable value);
}
Java
package com.moviecenter.models;

import com.enterlib.databinding.NotifyPropertyChanged;
import com.moviecenter.IImageLoader;

import android.graphics.drawable.Drawable;

public class Actor extends NotifyPropertyChanged
                   implements OnImageLoadedListener {

	public int Id;

	public String Name;

	public String LastName;

	public String Description;

	public String ImageFile;

	private Drawable mImage;

	public Actor(int id, String name, String lastName, String description,
			String imageFile) {
		super();
		Id = id;
		Name = name;
		LastName = lastName;
		Description = description;
		ImageFile = imageFile;
	}

	public Actor() {
	}

	/**This load the Drawable in another thread using the {@code loader}
	 * after the image is loaded it notifies the View with onPropertyChanges
	 * so the View can display the image
	 * @param loader Defines a contract for loading drawables
	 * */
	public Actor loadImageAsync(IImageLoader loader){
		loader.loadImageAsync(ImageFile, this);
		return this;
	}

	public Drawable getImage(){
		return mImage;
	}

	@Override
	public void setImage(Drawable value){
		mImage = value;

		//notifies the target property the value has changed
		onPropertyChange("Image");
	}

	public String getFullName(){
		return Name+" "+LastName;
	}

	@Override
	public String toString() {
		return getFullName();
	}
}

Next goes the Film model.

Java
package com.moviecenter.models;

import java.util.ArrayList;
import android.graphics.drawable.Drawable;

import com.enterlib.StringUtils;
import com.enterlib.annotations.DataMember;
import com.enterlib.databinding.NotifyPropertyChanged;
import com.moviecenter.IImageLoader;

public class Film extends NotifyPropertyChanged
                  implements OnImageLoadedListener{

	public int Id;

	public String Title;

	public int Year;

	public double Rating;

	public String Genre;

	public boolean IsAvailableForRent;

	public double Price;

	public String Description;

	public String ImageFile;

	private Drawable mImage;

	private ArrayList<Actor> mActors = new ArrayList<Actor>();

	@DataMember(listType=Actor.class)
	public ArrayList<Actor> getActors(){
		return mActors;
	}

	@DataMember(listType=Actor.class)
	public void setActors(ArrayList<Actor>actors){
		mActors = actors;
	}

	public Drawable getImage(){
		return mImage;
	}

	@Override
	public void setImage(Drawable value){
		mImage = value;
		onPropertyChange("Image");
	}

	public boolean getContainsGenre(){
		return !StringUtils.isNullOrWhitespace(Genre);
	}

	public Film loadImageAsync(IImageLoader loader){
		loader.loadImageAsync(ImageFile, this);
		return this;
	}
}

Finally, the RentOrder model for sending a rent order.

Java
package com.moviecenter.models;

import java.util.Date;

public class RentOrder {

	public int FilmId;

	public Date FromDate;

	public Date ToDate;

	public int Copies;

	public double Price;

	public UserInfo UserInfo;

	public int FormatTypeId;
}

I have also created the following class for the purpose of showing how data binding can be done with nested objects.

Java
package com.moviecenter.models;

public class UserInfo {

	public String Name;

	public String Email;

	public String Adress;
}

The Activity

The MainActivity displays the list of Films. Each Film has a check mark indicating whether it's available for rent.

Java
package com.moviecenter;

import android.app.Activity;
import android.os.Bundle;

public class MainActivity extends Activity {

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		if (savedInstanceState == null) {
			getFragmentManager()
				.beginTransaction()
				.add(R.id.container, new FragmentFilmList())
				.commit();
		}
	}
}

The MainActivity's layout contains just a FrameLayout as a placeholder for the Fragment. It's in the definition of FragmentFilmList where the magic takes place.

Java
package com.moviecenter;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;

import com.enterlib.converters.IValueConverter;
import com.enterlib.databinding.BindingResources;
import com.enterlib.exceptions.ConversionFailException;
import com.enterlib.fields.Field;
import com.enterlib.mvvm.FormFragment;
import com.enterlib.mvvm.SelectionCommand;
import com.moviecenter.models.Actor;
import com.moviecenter.models.Film;

public class FragmentFilmList extends FormFragment {
	ArrayList<Film> mFilms;
	ImageLoader mLoader;

	public SelectionCommand Selection = new SelectionCommand() {
		@Override
		public void invoke(Field field, AdapterView<!--?--> adapterView, View itemView,
				int position, long id) {

			//show the film details fragment
			Film f = (Film) adapterView.getItemAtPosition(position);

			getActivity().getFragmentManager()
			.beginTransaction()
			.replace(R.id.container, FragmentFilm.newIntance(f))
			.addToBackStack("FilmDetails")
			.commit();
		}
	};

	public FragmentFilmList() {
	}

	public List<<Film> getFilms(){
		return mFilms;
	}

	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container,
			Bundle savedInstanceState) {
		returns inflater.inflate(R.layout.fragment_main,
                                 container, false);
	}

	//register the converters with the BindingResources
	@Override
	protected BindingResources getBindingResources() {
		return new BindingResources()
			.put("CurrencyConverter", new IValueConverter() {
			/**convert a target property value to a source property value */
				@Override
				public Object convertBack(Object value)
						throws ConversionFailException {
					//just return null. It's not used in read only views
					return null;
				}
				/** convert a source property value to a target property value */
				@Override
				public Object convert(Object value)
						throws ConversionFailException {
					return String.format(Locale.getDefault(), "%,.2f $", value);
				}
			})
			.put("BoolToVisibility", new IValueConverter() {
				@Override
				public Object convertBack(Object value)
						throws ConversionFailException {
					//just return null. It's not used in read only views
					return null;
				}

				@Override
				public Object convert(Object value)
						throws ConversionFailException {
					return ((Boolean)value) == true ? View.VISIBLE:View.GONE;
				}
			});
	}

	@Override
	public void onStart() {
		super.onStart();

		//load the films list
		mFilms = loadFilms();

		//update the binding target properties
		updateTargets();
	}


	private ArrayList<Film> loadFilms() {
		//The code is omitted for simplicity
	}
}

Another good component is the IImageLoader implementation. This will enqueue the requested operations that load the images from the Assets.

Java
package com.moviecenter;

import java.io.IOException;
import java.io.InputStream;

import android.content.Context;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.graphics.drawable.BitmapDrawable;
import android.util.Log;

import com.enterlib.threading.LoaderHandler;
import com.enterlib.threading.LoaderHandler.LoadTask;
import com.moviecenter.models.OnImageLoadedListener;

public class ImageLoader implements IImageLoader {
	LoaderHandler mLoadHandler;
	Context mContext;
	Resources res;
	public ImageLoader(Context context) {
		mContext = context;
		res =mContext.getResources();
	}

	//Load each image asynchronously one after another.
	@Override
	public void loadImageAsync(String imageFile, final OnImageLoadedListener listener) {
		if(mLoadHandler==null){
			mLoadHandler = new LoaderHandler();
		}

		mLoadHandler.postTask(new LoadTask() {

			//This method is called on the LoaderHandler thread
			@Override
			public Object runAsync(Object args) throws Exception {
				String imageFile = (String)args;
				AssetManager assets = mContext.getAssets();
				InputStream is;
				if(imageFile == null){
					is = assets.open("actor.png");
					return new BitmapDrawable(res, is);
				}

				try{
					is = assets.open(imageFile);
				}catch(IOException e){
					is = assets.open("actor.png");
				}
				return new BitmapDrawable(res, is);
			}

			//This method is called on the UI thread and after the runAsync finished
			//or and Exception was thrown.
			@Override
			public void onComplete(Object result, Exception e) {
				if(e!=null){
					Log.d(getClass().getName(), e.getMessage(), e);
					return;
				}
				listener.setImage((BitmapDrawable)result);

			}
		}, imageFile);
	}
}

The FragmentFilmList also defines the Selection command that is invoked for showing the films details fragment. A SelectionCommand can be binded to the ItemClickCommand BindingProperty defined in the ListField class.

The fragment_main XML

XML
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
   >

    <ListView
	    android:id="@+id/listView"
	    android:layout_width="match_parent"
	    android:layout_height="match_parent"
	    android:layout_marginBottom="5dp"
	    android:dividerHeight="1dp"
	    android:choiceMode="singleChoice"
	    android:fastScrollEnabled="true"
	    android:tag="{
               Value:Films,
               ItemTemplate:template_film,
               ItemClickCommand:Selection}"
	     />

</RelativeLayout>

The template_film.xml

XML
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
       			android:layout_width="match_parent"
  			    android:layout_height="match_parent"
  			    android:paddingTop="10dp"
  			    android:paddingBottom="10dp" >

	    <ImageView
            android:layout_width="120dp"
            android:layout_height="90dp"
            android:layout_alignParentLeft="true"
            android:layout_alignParentStart="true"
            android:layout_centerVertical="true"
            android:id="@+id/imageView1"
            android:scaleType="fitXY"
            android:tag="{Value:Image}" />

	    <LinearLayout
            android:id="@+id/descriptionPanel"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_alignParentTop="true"
            android:layout_marginLeft="5dp"
            android:layout_toRightOf="@+id/imageView1"
            android:orientation="vertical" >

	        <!-- Title -->
	        <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:singleLine="true"
                android:ellipsize="end"
                android:textAppearance="@android:style/TextAppearance.DeviceDefault.Large"
                android:tag="{Value:Title}" />

	        <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="horizontal">

	             <!-- Rating -->
	               <TextView android:layout_width="wrap_content"
                             android:layout_height="wrap_content"
                             android:text="Rating:"/>

	               <TextView android:layout_width="wrap_content"
                             android:layout_height="wrap_content"
                             android:layout_marginLeft="5dp"
                             android:tag="{Value:Rating}"/>

	                <!-- Year -->
	               <TextView
                             android:layout_marginLeft="10dp"
                             android:layout_width="wrap_content"
                             android:layout_height="wrap_content"
                             android:text="Year:"/>

	               <TextView android:layout_width="wrap_content"
                             android:layout_height="wrap_content"
                             android:layout_marginLeft="5dp"
                             android:tag="{Value:Year}"/>

	        </LinearLayout>

	        <!-- Genre -->
	        <LinearLayout android:layout_width="match_parent"
                             android:layout_height="wrap_content"
                             android:orientation="horizontal"
                             android:tag="{Visibility:{Source:ContainsGenre,
                                           Converter:BoolToVisibility} }">

	               <TextView android:layout_width="wrap_content"
                             android:layout_height="wrap_content"
                             android:text="Genre:"/>

	               <TextView android:layout_width="wrap_content"
                             android:layout_height="wrap_content"
                             android:layout_marginLeft="5dp"
                             android:tag="{Value:Genre}"/>
	        </LinearLayout>

	        <!-- Price -->
	        <LinearLayout android:layout_width="match_parent"
	            android:layout_height="wrap_content"
	            android:orientation="horizontal">
	               <TextView android:layout_width="wrap_content"
                             android:layout_height="wrap_content"
                             android:text="Price:"/>

	               <TextView android:layout_width="0dp"
                             android:layout_height="wrap_content"
                             android:layout_marginLeft="5dp"
                             android:layout_weight="1"
                             android:tag="{Value:Price,
                                           Converter:CurrencyConverter }"/>

	               <!-- IsAvailableForRent -->
	               <CheckBox android:layout_width="wrap_content"
                             android:layout_height="wrap_content"
                             android:clickable="false"
                             android:focusable="false"
                             android:tag="{Value:IsAvailableForRent}"/>
	        </LinearLayout>

	    </LinearLayout>
 </RelativeLayout>

You can see how with a simple code, you can create a rich user interface and let you focus on the business.

Java
package com.moviecenter;

import java.util.Locale;

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import com.enterlib.converters.IValueConverter;
import com.enterlib.databinding.BindingResources;
import com.enterlib.exceptions.ConversionFailException;
import com.enterlib.mvvm.Command;
import com.enterlib.mvvm.FormFragment;
import com.enterlib.serialization.JSonSerializer;
import com.moviecenter.models.Actor;
import com.moviecenter.models.Film;

public class FragmentFilm extends FormFragment {

	static final String FILM = "FILM";

	Film mFilm;
	ImageLoader mLoader;

	public Film getFilm(){
		return mFilm;
	}

	public static FragmentFilm newIntance(Film film){
		Bundle args = new Bundle();
		args.putString(FILM, JSonSerializer.serializeObject(film));
		FragmentFilm fragment = new FragmentFilm();
		fragment.setArguments(args);
		return fragment;
	}

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);

		mFilm =JSonSerializer.deserializeObject(Film.class,
				getArguments().getString(FILM));

		//Disable the command if the Film is not available for rent
		//This also disable the button binded to the command
		RentFilm.setEnabled(mFilm.IsAvailableForRent);
	}

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

		return inflater.inflate(R.layout.fragment_film, container,false);
	}

	@Override
	public void onStart() {
		super.onStart();

		loadImages();
		updateTargets();
	}

	private void loadImages() {
		if(mLoader==null)
			mLoader = new ImageLoader(getActivity());

		mFilm.loadImageAsync(mLoader);

		for (Actor actor : mFilm.getActors()) {
			actor.loadImageAsync(mLoader);
		}
	}

	public Command RentFilm = new Command() {
		@Override
		public void invoke(Object invocator, Object args) {
			getActivity().getFragmentManager()
			.beginTransaction()
			.replace(R.id.container, FragmentRentFilm.newIntance(mFilm))
			.addToBackStack("RentOrder")
			.commit();
		}
	};

	@Override
	protected BindingResources getBindingResources() {

		//register the converters with the BindingResources
		return new BindingResources()
				.put("CurrencyConverter", new IValueConverter() {

					@Override
					public Object convertBack(Object value) 
                                             throws ConversionFailException {
						return null;
					}

					@Override
					public Object convert(Object value) 
                                             throws ConversionFailException {
						return String.format(Locale.getDefault(), 
                                                                     "%,.2f$", value);
					}
				})
				.put("BoolToVisibility", new IValueConverter() {

					//not used in read only views
					@Override
					public Object convertBack(Object value) 
                                            throws ConversionFailException {
						return null;
					}

					@Override
					public Object convert(Object value) 
                                            throws ConversionFailException {
						return ((Boolean)value) == true ? 
                                                         View.VISIBLE:View.GONE;
					}
				});
	}
}

Now I want to show a nice feature of data binding with the fragment_film.xml layout. But first, look at the markup and note the last LinearLayout.

The fragment_film.xml

XML
<?xml version="1.0" encoding="utf-8"?>
<ScrollView  xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:tag="{Value:Film}" >

         <ImageView
            android:id="@+id/imageView1"
            android:adjustViewBounds="true"
	      	android:layout_width="fill_parent"
	        android:layout_height="wrap_content"
	        android:cropToPadding="true"
	         android:baselineAlignBottom="false"
            android:scaleType="fitXY"
	      	android:src="@drawable/film"
	      	android:tag="{Value:Image}" />


          <LinearLayout
	        android:id="@+id/descriptionPanel"
	        android:layout_width="match_parent"
	        android:layout_height="wrap_content"
	        android:layout_alignParentRight="true"
	        android:layout_alignParentTop="true"
	        android:layout_marginLeft="5dp"
	        android:layout_marginStart="5dp"
	        android:layout_toEndOf="@+id/imageView1"
	        android:layout_toRightOf="@+id/imageView1"
	        android:orientation="vertical" >

	        <!-- Title -->
	        <TextView
	            android:layout_width="match_parent"
	            android:layout_height="wrap_content"
	            android:text="Start War"
	            android:gravity="center"
	            android:textAppearance="@android:style/TextAppearance.DeviceDefault.Large"
	            android:tag="{Value:Title}" />

	        <Button
	            android:layout_width="wrap_content"
	            android:layout_height="wrap_content"
	            android:text="Order Film"
	            android:tag="{ClickCommand:RentFilm}" />

	        <LinearLayout
	            android:layout_width="match_parent"
	            android:layout_height="wrap_content"
	            android:orientation="horizontal"
	            android:layout_marginTop="3dp">

	             <!-- Rating -->
	               <TextView android:layout_width="wrap_content"
	            			 android:layout_height="wrap_content"
				             android:text="Rating:"/>

	               <TextView android:layout_width="wrap_content"
	            			 android:layout_height="wrap_content"
	            			 android:layout_marginLeft="5dp"
				             android:tag="{Value:Rating}"/>

	                <!-- Year -->
	               <TextView
	                   		 android:layout_marginLeft="10dp"
	                   		 android:layout_width="wrap_content"
	            			 android:layout_height="wrap_content"
				             android:text="Year:"/>

	               <TextView android:layout_width="wrap_content"
	            			 android:layout_height="wrap_content"
	            			 android:layout_marginLeft="5dp"
				             android:tag="{Value:Year}"/>

	        </LinearLayout>

	        <!-- Genre -->
	        <LinearLayout android:layout_width="match_parent"
	            android:layout_height="wrap_content"
	            android:orientation="horizontal"
	            android:tag="{ Visibility:{Source:ContainsGenre, Converter:BoolToVisibility} }"
	            android:layout_marginTop="3dp" >

	               <TextView android:layout_width="wrap_content"
	            			 android:layout_height="wrap_content"
				             android:text="Genre:"/>

	               <TextView android:layout_width="wrap_content"
	            			 android:layout_height="wrap_content"
	            			 android:layout_marginLeft="5dp"
				             android:tag="{Value:Genre}"/>
	        </LinearLayout>

	        <!-- Price -->
	        <LinearLayout android:layout_width="match_parent"
	            android:layout_height="wrap_content"
	            android:orientation="horizontal"
	            android:layout_marginTop="3dp">
	               <TextView android:layout_width="wrap_content"
	            			 android:layout_height="wrap_content"
				             android:text="Price:"/>

	               <TextView android:layout_width="wrap_content"
	            			 android:layout_height="wrap_content"
	            			 android:layout_marginLeft="5dp"
	            			 android:text="7"
	            			 android:layout_weight="1"
				         android:tag="{Value:Price, Converter:CurrencyConverter }"/>


	        </LinearLayout>

	    </LinearLayout>

          <TextView
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:textStyle="bold"
              android:layout_marginLeft="5dp"
	        android:layout_marginStart="5dp"
              android:text="Description:" />

          <TextView
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:layout_marginLeft="5dp"
	          android:layout_marginStart="5dp"
              android:textStyle="italic"
              android:minLines="2"
              android:tag="{Value:Description}" />

          <TextView
              android:layout_marginTop="5dp"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:textStyle="bold"
              android:layout_marginLeft="5dp"
	          android:layout_marginStart="5dp"
              android:text="Actores:" />

          <LinearLayout
	        android:orientation="vertical"
	        android:layout_width="match_parent"
	        android:layout_height="wrap_content"
	        android:paddingLeft="10dp"
	        android:paddingRight="10dp"
	        android:tag="{Value:Actors, ItemTemplate:template_actor}" />

</LinearLayout>
</ScrollView>

In the last LinearLayout of the previous XML is defined an ItemTemplate in the binding expression. So this means the ItemTemplate can be used also with any ViewGroup.

The template_actor.xml

XML
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
       			android:layout_width="match_parent"
  			    android:layout_height="match_parent"
  			    android:paddingTop="10dp"
  			    android:paddingBottom="10dp">

	    <ImageView
	      	android:layout_width="90dp"
	        android:layout_height="67dp"
	        android:layout_alignParentLeft="true"
	        android:layout_alignParentTop="true"
	        android:id="@+id/imageView1"
	      	android:scaleType="fitXY"
	      	android:tag="{Value:Image}" />

	    <LinearLayout
	        android:id="@+id/descriptionPanel"
	        android:layout_width="wrap_content"
	        android:layout_height="wrap_content"
	        android:layout_alignParentRight="true"
	        android:layout_alignParentTop="true"
	        android:layout_marginLeft="5dp"
	        android:layout_toRightOf="@+id/imageView1"
	        android:orientation="vertical" >

	        <!-- Fullname -->
	        <TextView
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:textAppearance="@android:style/TextAppearance.DeviceDefault.Medium"
                 android:tag="{Value:FullName}" />

	       <!-- Description -->
	       <TextView android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:layout_marginTop="5dp"
                 android:tag="{Value:Description}"/>

	    </LinearLayout>
 </RelativeLayout>

Until now, we have seen how data binding is utilized with read-only views. Following it will be used in editing views, so let's define the Fragment for sending a RentOrder item.

The FragmentRentFilm will contain the logic for submitting a RentOrder. Besides, it will load additional data like, for example, the list of discs format the film may be delivered into. On the other hand, you can see how easy validations are performed, for example, an EmailValidator entry is registered with a RegExValueValidator object and used with an EditText, also the EmailValidator can be reused in another views promoting reusability.

Java
package com.moviecenter;

import java.util.Date;

import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;

import com.enterlib.DialogUtil;
import com.enterlib.converters.DoubleToStringConverter;
import com.enterlib.converters.IntegerToStringConverter;
import com.enterlib.data.BaseModelComparer;
import com.enterlib.data.BaseModelConverter;
import com.enterlib.data.IdNameValue;
import com.enterlib.databinding.BindingResources;
import com.enterlib.mvvm.Command;
import com.enterlib.mvvm.FormFragment;
import com.enterlib.serialization.JSonSerializer;
import com.enterlib.validations.validators.RegExValueValidator;
import com.moviecenter.models.Film;
import com.moviecenter.models.RentOrder;
import com.moviecenter.models.UserInfo;

public class FragmentRentFilm extends FormFragment {
	static final String FILM = "FILM";

	Film mFilm;
	RentOrder mOrder;

	//The list of disc formats available
	public IdNameValue[] getFormats(){
		return new IdNameValue[]{
				new IdNameValue(1, "4.5 GB DVD"),
				new IdNameValue(2, "8 GB DVD"),
				new IdNameValue(3, "Blue Ray"),
		};
	}

	public String getFilmName(){
		return mFilm.Title;
	}

	public RentOrder getOrder(){
		return mOrder;
	}

	public static FragmentRentFilm newIntance(Film film){
		Bundle args = new Bundle();
		args.putString(FILM, JSonSerializer.serializeObject(film));
		FragmentRentFilm fragment = new FragmentRentFilm();
		fragment.setArguments(args);
		return fragment;
	}

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);

		//load the data. It can also be loaded in onActivityCreated or onStart
		//but in this case is so simple that can be done in onCreate
		mFilm =JSonSerializer.deserializeObject(Film.class,
				getArguments().getString(FILM));

		mOrder = new RentOrder();
		mOrder.FilmId = mFilm.Id;
		mOrder.Copies = 1;
		mOrder.FromDate = new Date();
		mOrder.Price = mFilm.Price;
		mOrder.UserInfo = new UserInfo();
	}

	@Override
	public View onCreateView(LayoutInflater inflater, ViewGroup container,
			Bundle savedInstanceState) {
		return inflater.inflate(R.layout.fragment_rent_film, container,false);
	}

	@Override
	public void onStart() {
		super.onStart();
		updateTargets();
	}

	public Command Submit = new Command() {
		@Override
		public void invoke(Object invocator, Object args) {

			if(validate()){
				//Do something with the Order

				 Toast.makeText(getActivity(),
						 "The Order is on the way", Toast.LENGTH_SHORT).show();

				 String json = JSonSerializer.serializeObject(mOrder);
				 Log.d(getClass().getName(), json);

				 DialogUtil.showAlertDialog(getActivity(), "Result",
						 json, new OnClickListener() {
							@Override
							public void onClick(DialogInterface dialog, 
                                                             int which) {
								getActivity().getFragmentManager().
                                                                                   popBackStack();
							}
						});
			}
		}
	};

	@Override
	protected BindingResources getBindingResources() {
	   //register the binding resources like
	   //type converters ,validators and comparators
		return new BindingResources()
			.put("IntConverter", new IntegerToStringConverter())
			.put("DoubleConverter", new DoubleToStringConverter())
			.put("EmailValidator", new RegExValueValidator
                              ("(\\w+)(\\.(\\w+))*@(\\w+)(\\.(\\w+))*", "Invalid Email"))
			.put("ModelComparer", new BaseModelComparer())
			.put("ModelToIdConverter", new BaseModelConverter());
	}
}

The fragment_rent_film.xml

XML
<?xml version="1.0" encoding="utf-8"?>
<ScrollView  xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:tag="{Value:Order}" >

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Film Name"
        android:gravity="center_horizontal"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:tag="{Value:FilmName}" />

    <!-- Copies -->
    <TextView
        android:layout_marginTop="3dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Number of Copies:" />

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="number"
        android:tag="{Value:Copies, Converter:IntConverter}" />

    <!-- Price -->
    <TextView
        android:layout_marginTop="3dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Cost:" />

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="numberDecimal"
        android:enabled="false"
        android:tag="{Value:Price, Converter:DoubleConverter}" />

	<!-- From -->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="From:" />

    <com.enterlib.widgets.DatePickerButton
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:tag="{Value:FromDate, Required:true}"/>

    <!-- To -->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="To:" />

    <com.enterlib.widgets.DatePickerButton
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:tag="{Value:ToDate, Required:true}"/>

     <!-- Formats -->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Format:" />

    <Spinner android:id="@+id/spFormat"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:tag="{Value:FormatTypeId,
                      Items:Formats,
                      Comparer:ModelComparer,
                      Converter:ModelToIdConverter,
                      Required:true}"/>

     <!-- UserInfo -->
    <LinearLayout
        android:layout_margin="5dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:tag="{Value:UserInfo}">

	    <!-- Name -->
	    <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Name:" />
	    <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:inputType="textPersonName"
            android:tag="{Value:Name, Required:true}" />

	    <!-- Email -->
	    <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Email:" />
	    <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:inputType="textEmailAddress"
            android:tag="{Value:Email,
                          Required:true,
                          Validators:[EmailValidator]}" />

	     <!-- Adress -->
	    <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Adress:" />
	    <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:inputType="textPersonName"
            android:tag="{Value:Adress, Required:true}" />

    </LinearLayout>

    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:text="Submit Order"
        android:tag="{ClickCommand:Submit}" />

</LinearLayout>
</ScrollView>

And finally, the IdNameValue definition seen earlier:

Java
package com.enterlib.data;

import java.io.Serializable;

public class BaseModel implements Serializable {

	public int id;

	public BaseModel(int id) {
		this.id = id;
	}

	public BaseModel() {
	}
}
Java
public class IdNameValue extends BaseModel implements Serializable {

	public String name;

	public IdNameValue() {
	}

	public IdNameValue(int id, String name) {
		this.id = id;
		this.name = name;
	}
}

Film List Screens

Film Details Screens

Sent Rent Order Screens

Points of Interest

The Enterlib's github repository is out of date, so with the sample project's source code ships out a compiled updated version of the library you can use under the CPOL licence.

About the Author

My name is Ansel Castro Cabrera, I'm a software developer and a graduate of Computer Science. I began coding Enterlib when I started developing Android enterprise applications as a freelancer. I also like doing research involving deep learning, computer vision, and computer graphics. Although I like Java and other languages, I must say I'm a native C# and .NET developer. I also like sports like swimming, cycling and painting too.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Uruguay Uruguay
Senior Software Engineer with more than 8 years of experience in the industry. Graduated from Computer Science ,focused on .NET and Java technologies with special interest on Computer Graphics, Compilers , Languages and Machine Learning.

Comments and Discussions

 
-- There are no messages in this forum --