Scalable Data Binding Framework for Android






3.67/5 (4 votes)
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
.
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 fullField
. ABindingProperty
must be defined asstatic
members of aField
class and they are inherited for its descendant classes. For example, theField
class defines the binding propertiesValue
,Required
,Value
,Restorable
,Converter
amount others. This allows a scalable binding mechanism the user can extend by defining newField
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 aField
or its linkedView
. - 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 linkedView
. - Source Object: The source object declares the source properties. It can be the
ViewModel
when creating theForm
, the object inForm.updateTargets(Object)
,Form.updateSource(Object)
or any object accessible from theViewModel
.
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
.
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
.
<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:
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:
<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 theItemsField
base class ofSpinnerField
. Use it in order to retrieve the elements shown in theSpinner
’s dropdown list, aListView
or anyViewGroup
. - Comparer: Is a
BindingProperty
forSelectionField
like theSpinnerField
. Can be used for setting the selected position of theSpinner
. It will compare the objects in Items with the object ofValue
. The reference forComparer
can be resolved from theBindingResource
or theViewModel
if it's not found in the first one. - Converter: Is a
BindingProperty
defined inField
. Use it for set anIValueConverted
between thetarget
andsource
properties. In the example above, the target property "Value
" returns aCategory
object but thesource
property expects an integer, so theConverter
will get the id from theCategory
. The reference forConverter
can be resolved from theBindingResource
or theViewModel
if it's not found in the first one. - ItemTemplate: A
BindingProperty
defined in theItemsField
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 anIValueConverter
using theConverter
keyword for converting aBoolean
from thesource
property to anInteger
of thetarget
property. Note in this case, thesource
property is binded using theSource
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.
package com.moviecenter.models;
import android.graphics.drawable.Drawable;
public interface OnImageLoadedListener {
void setImage(Drawable value);
}
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.
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.
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.
package com.moviecenter.models;
public class UserInfo {
public String Name;
public String Email;
public String Adress;
}
The Activity
The MainActivity
displays the list of Film
s. Each Film
has a check mark indicating whether it's available for rent.
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.
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.
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
<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 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.
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 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 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.
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 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:
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() {
}
}
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.