65.9K
CodeProject 正在变化。 阅读更多。
Home

Android 可扩展数据绑定框架

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.67/5 (4投票s)

2016 年 3 月 28 日

CPOL

9分钟阅读

viewsIcon

12885

downloadIcon

39

描述了 Android 的可扩展数据绑定实现。

引言

数据绑定被认为是任何用于开发业务应用程序的框架中最受欢迎的功能之一。它通常允许程序员通过声明式表达式将数据与 UI 元素绑定。这种便利性节省了编写繁琐代码的大量时间。在之前的文章中,我们已经讨论了 Enterlib 的一些方面,这是一个用于 Android 的 Model-View-ViewModel 框架。那篇文章介绍了一些其数据绑定功能和绑定表达式的示例。因此,我建议先阅读文章 A MVVM framework for Android - Enterlib 以获得该框架的概述,然后再阅读本文。

背景

本节将简要介绍 Enterlib 数据绑定基础设施的核心组件,这些组件将在全文中不断引用。

在此数据绑定实现的中心是 Field 类,该类旨在扩展 UI 元素的属性,例如 View 类。此外,它还负责在视图级别支持数据绑定、验证和其他功能。因此,它会将视图层次结构包装到字段层次结构中,以用于分配了绑定表达式的视图。在撰写本文时,验证仅支持 Field 的一个特殊属性,名为 Value,主要原因是当时没有必要支持所有 View 的属性。该框架定义了几个扩展 Field 的具体类,它们重写了相应视图的 Value 属性。此外,开发人员可以注册自己的 FieldFactory,以便为正在处理的自定义 View 提供所需的 Field。但是,如果没有 FieldFactory 来为给定的 View 提供 Field,则默认会创建一个 GenericField 并将其链接到 ViewGenericField 启用了所有数据绑定功能,但 Value 属性将始终返回 null,因此涉及此属性的数据绑定将无效。

第二个重要类是 Form。使用此类来访问绑定框架和字段层次结构。此外,该框架还定义了 FormFragment,它利用 Form 的管理功能,并将 Form 的实例化和状态保存插入到 Fragment 的生命周期中的适当位置。如果要使用 MVVM 架构,那么可以继承 BindableFragment 或其任何子类,例如 BindableEditFragmentBindableDialogFragmentBindableEditFragmentListFragment

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();
}

如前所述,Form 是使用 Form.build(…) 创建的,这将处理视图层次结构并创建相应的 Fields。您可以将 viewModel 作为参数传递,以及可选的 BindingResources,它为绑定框架提供额外的信息,例如 IValueConvertersIValueValidators 等实例。

使用 Form,您可以注册 IValidator 实例来执行涉及多个字段的验证逻辑。例如,可能需要一个日期必须早于另一个日期,或者一个字段的值必须匹配另一个字段的值。所有之前的验证都在视图层完成,另一方面,有些验证必须在业务层完成。因此,该框架提供了一种通过从业务层抛出 ValidationException 来将这些验证消息路由回 UI 的方法。ValidationException 包含一个 ErrorInfo,可以将其与 Form.setFieldErrors 一起传递给 Form,以在 UI 中反映这些验证消息。

属性,如 Java Bean 术语中常用的,是任何以 "get" 或 "is" 作为前缀且没有参数的 public 实例成员或 public 实例方法。或者任何以 "set" 作为前缀且只有一个参数的 public 实例方法。属性可能具有 getter 和 setter,属性的示例包括 Value(带有 getValue()setValue(Object))、Enabled(带有 isEnabled()setEnabled(bool))、Visibility(带有 getVisibility(int)setVisibility(int))等。

其他概念包括

  • BindingProperty:绑定属性是属性概念的扩展。它用于初始化其相应的绑定表达式成员或指定目标属性或整个 Field 的某些行为。BindingProperty 必须定义为 Field 类的 static 成员,并且会被其后代类继承。例如,Field 类定义了绑定属性 ValueRequiredValueRestorableConverter 等。这允许用户通过定义新的 Field 类以及必要的绑定属性来扩展可伸缩的绑定机制。
  • 目标属性:目标属性与 UI 元素相关,可以是 BindingProperty,也可以是声明在 Field 或其链接的 View 中的普通属性。
  • 源属性:源属性是源对象的任何属性。
  • 目标对象:目标是声明目标属性的对象,例如 Field 或其链接的 View
  • 源对象:源对象声明源属性。它可以是创建 Form 时的 ViewModelForm.updateTargets(Object)Form.updateSource(Object) 中的对象,或者是从 ViewModel 可访问的任何对象。

以下是 Field 类中定义的绑定属性的一些示例。在绑定表达式解析后,set 方法会使用由 ExpressionMember 表示的绑定表达式成员来调用。此外,Field 实现 IPropertyChangedListener,因此当其绑定的 source 属性更改其 value 时,可以通知它来更新 target 属性。

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");
}

绑定表达式示例

在下面的示例中,从 ViewModelPerson 属性返回的对象被绑定到父 LinearLayout,该对象的 Name 属性被绑定到 EditTextValue 属性。实际上,LinearLayoutEditText 都没有定义 Value 属性,但框架知道它是相关 Field 的属性。EditText 还定义了其值是必需的,在这种情况下,使用了 BindingProperty "Required"。此外,绑定表达式可以使用未在 Field 中定义,但在其 View 中定义的属性,如 VisibilityEnabled

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

绑定表达式语法

下面显示了绑定表达式的语法

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

ID = 目标属性名
语法定义中使用的符号的含义是

  • () 用于分组元素
  • | 指定多个选项
  • * 指定零个或多个元素

标记是 { } : [ ] ,。最后一个标记用于分隔绑定表达式成员。

一个更复杂的例子

<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}}" /> 

在下面的示例中,框架为 Spinner 实例化一个 SpinnerField。绑定表达式中引用的某些目标属性如下所述

  • Items:一个 BindingProperty,定义在 SpinnerField 的基类 ItemsField 中。使用它来检索 Spinner 下拉列表、ListView 或任何 ViewGroup 中显示的元素。
  • Comparer:对于 SelectionField(如 SpinnerField)是一个 BindingProperty。可用于设置 Spinner 的选定位置。它将比较 Items 中的对象与 Value 中的对象。Comparer 的引用可以从 BindingResource 解析,如果它在第一个中找不到,则可以从 ViewModel 中解析。
  • Converter:一个定义在 Field 中的 BindingProperty。使用它来在 targetsource 属性之间设置一个 IValueConverted。在上面的示例中,目标属性 "Value" 返回一个 Category 对象,但 source 属性期望一个整数,因此 Converter 将从 Category 中获取 ID。Converter 的引用可以从 BindingResource 解析,如果它在第一个中找不到,则可以从 ViewModel 中解析。
  • ItemTemplate:一个定义在 ItemsField 基类中的 BindingProperty。使用它为显示 Items 指定自定义布局。
  • Visibility:一个定义在 View 类中的普通属性。可选地,您可以使用 Converter 关键字设置一个 IValueConverter,用于将 source 属性的 Boolean 转换为 target 属性的 Integer。请注意,在这种情况下,source 属性是使用 Source 关键字绑定的。

Using the Code

下面的示例将展示如何使用数据绑定。为简单起见,它将在没有 MVVM 基础设施的情况下使用,但正如您之前看到的,它可以很好地集成。该示例将涵盖一个电影中心应用程序的开发,用于租借电影。那么让我们开始定义我们的业务模型和契约。

电影中心业务模型

此接口定义了设置图像的契约,以及 FilmActor 模型实现。

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();
	}
}

接下来是 Film 模型。

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;
	}
}

最后是用于发送租借订单的 RentOrder 模型。

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;
}

我还创建了以下类,用于演示如何使用嵌套对象进行数据绑定。

package com.moviecenter.models;

public class UserInfo {

	public String Name;

	public String Email;

	public String Adress;
}

Activity

MainActivity 显示 Film 列表。每部 Film 都有一个复选标记,表示是否可供租借。

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();
		}
	}
}

MainActivity 的布局仅包含一个 FrameLayout 作为 Fragment 的占位符。魔法发生在 FragmentFilmList 的定义中。

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
	}
}

另一个很好的组件是 IImageLoader 实现。这将排队请求的操作,这些操作会从 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);
	}
}

FragmentFilmList 还定义了 Selection 命令,该命令在点击时用于显示电影详情片段。SelectionCommand 可以绑定到 ListField 类中定义的 ItemClickCommand BindingProperty

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>

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>

您可以看到,通过简单的代码,您可以创建丰富的用户界面,让您可以专注于业务。

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;
					}
				});
	}
}

现在我想展示数据绑定的一项很棒的功能,通过 fragment_film.xml 布局。但首先,请看标记,并注意最后的 LinearLayout

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>

在上一个 XML 的最后一个 LinearLayout 中,绑定表达式中定义了一个 ItemTemplate。这意味着 ItemTemplate 也可以与任何 ViewGroup 一起使用。

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>

到目前为止,我们已经看到了数据绑定如何与只读视图一起使用。接下来将在编辑视图中使用,所以让我们定义用于发送 RentOrder 项目的 Fragment

FragmentRentFilm 将包含提交 RentOrder 的逻辑。此外,它还将加载额外的数据,例如电影可能提供的光盘格式列表。另一方面,您可以看到验证是如何轻松执行的,例如,使用 RegExValueValidator 对象注册了一个 EmailValidator 条目,并将其用于 EditText,此外,EmailValidator 可以在其他视图中重用,从而促进了可重用性。

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());
	}
}

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>

最后,前面看到的 IdNameValue 定义

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;
	}
}

电影列表屏幕

电影详情屏幕

已发送的租借订单屏幕

关注点

Enterlib 的 github 存储库已过时,因此随附的示例项目源代码包含该库的编译更新版本,您可以在 CPOL 许可下使用。

关于作者

我的名字是 Ansel Castro Cabrera,我是一名软件开发人员,计算机科学专业毕业生。当我开始作为自由职业者开发 Android 企业应用程序时,我开始编写 Enterlib。我也喜欢进行涉及深度学习、计算机视觉和计算机图形学的研究。虽然我喜欢 Java 和其他语言,但我必须说我是 C# 和 .NET 的原生开发人员。我也喜欢游泳、骑自行车和绘画等运动。

© . All rights reserved.