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 {
public static interface FieldFactory {
Field createField(Class<?> viewClass, View view);
}
public static void addFactories(FieldFactory... factories);
public void updateTargets();
public void updateTargets(Object sourceObject);
public void updateSource() ;
public void updateSource(Object sourceObject);
public void setFieldErrors(ErrorInfo ei);
public void restoreState(Bundle savedInstanceState);
public void saveState(Bundle outState);
public void bindView(View view);
public static Form build(BindingResources bindingResources, View rootView,
Object viewModel);
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
.
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) {
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) {
object.setRequired(value.isValueTrue());
}
});
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 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.
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() {
}
public Actor loadImageAsync(IImageLoader loader){
loader.loadImageAsync(ImageFile, this);
return this;
}
public Drawable getImage(){
return mImage;
}
@Override
public void setImage(Drawable value){
mImage = value;
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) {
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);
}
@Override
protected BindingResources getBindingResources() {
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() {
@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;
}
});
}
@Override
public void onStart() {
super.onStart();
mFilms = loadFilms();
updateTargets();
}
private ArrayList<Film> loadFilms() {
}
}
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();
}
@Override
public void loadImageAsync(String imageFile, final OnImageLoadedListener listener) {
if(mLoadHandler==null){
mLoadHandler = new LoaderHandler();
}
mLoadHandler.postTask(new LoadTask() {
@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);
}
@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
="1.0"="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" >
<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">
<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}"/>
<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>
<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>
<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 }"/>
<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));
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() {
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() {
@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
="1.0"="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" >
<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">
<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}"/>
<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>
<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>
<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
="1.0"="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" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@android:style/TextAppearance.DeviceDefault.Medium"
android:tag="{Value:FullName}" />
<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;
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);
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()){
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() {
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
="1.0"="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}" />
<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}" />
<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}" />
<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}"/>
<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}"/>
<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}"/>
<LinearLayout
android:layout_margin="5dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:tag="{Value:UserInfo}">
<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}" />
<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]}" />
<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.