Android 可扩展数据绑定框架






3.67/5 (4投票s)
描述了 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
并将其链接到 View
。GenericField
启用了所有数据绑定功能,但 Value
属性将始终返回 null
,因此涉及此属性的数据绑定将无效。
第二个重要类是 Form
。使用此类来访问绑定框架和字段层次结构。此外,该框架还定义了 FormFragment
,它利用 Form
的管理功能,并将 Form
的实例化和状态保存插入到 Fragment 的生命周期中的适当位置。如果要使用 MVVM 架构,那么可以继承 BindableFragment
或其任何子类,例如 BindableEditFragment
、BindableDialogFragment
、BindableEditFragment
或 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();
}
如前所述,Form
是使用 Form.build(…)
创建的,这将处理视图层次结构并创建相应的 Fields。您可以将 viewModel
作为参数传递,以及可选的 BindingResources
,它为绑定框架提供额外的信息,例如 IValueConverters
、IValueValidators
等实例。
使用 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
类定义了绑定属性Value
、Required
、Value
、Restorable
、Converter
等。这允许用户通过定义新的Field
类以及必要的绑定属性来扩展可伸缩的绑定机制。- 目标属性:目标属性与 UI 元素相关,可以是
BindingProperty
,也可以是声明在Field
或其链接的View
中的普通属性。 - 源属性:源属性是源对象的任何属性。
- 目标对象:目标是声明目标属性的对象,例如
Field
或其链接的View
。 - 源对象:源对象声明源属性。它可以是创建
Form
时的ViewModel
、Form.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");
}
绑定表达式示例
在下面的示例中,从 ViewModel
的 Person
属性返回的对象被绑定到父 LinearLayout
,该对象的 Name
属性被绑定到 EditText
的 Value
属性。实际上,LinearLayout
和 EditText
都没有定义 Value
属性,但框架知道它是相关 Field
的属性。EditText
还定义了其值是必需的,在这种情况下,使用了 BindingProperty
"Required
"。此外,绑定表达式可以使用未在 Field
中定义,但在其 View
中定义的属性,如 Visibility
或 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>
绑定表达式语法
下面显示了绑定表达式的语法
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
。使用它来在target
和source
属性之间设置一个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 基础设施的情况下使用,但正如您之前看到的,它可以很好地集成。该示例将涵盖一个电影中心应用程序的开发,用于租借电影。那么让我们开始定义我们的业务模型和契约。
电影中心业务模型
此接口定义了设置图像的契约,以及 Film
和 Actor
模型实现。
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 的原生开发人员。我也喜欢游泳、骑自行车和绘画等运动。