小白7码-Android原生开发皮毛系列(5)-Form实例

1,199 阅读13分钟

临近年底,小白这段时间略忙,关于Android原生开发皮毛系列也停滞了一段时间,略感抱歉。 趁着略有空闲,小白打算对前面的章节做一个实践性的综合性的回顾,当然也会附带一些新的东西。这一节小白打算一个简单小家庭图书馆图书信息的录入表单页面作为实例做展开。其中会用到一些前面章节介绍的一些实例和用法,如果大家遗忘,不妨略花点时间做一个简略的回顾:

  1. 小白7码-android原生开发皮毛系列-TextView

  2. 小白7码-android原生开发皮毛系列(2)-Button以及Checkbox

  3. 小白7码-android原生开发皮毛系列(3)-Switch

  4. 小白7码-android原生开发皮毛系列(4)-Spinner

Android原生Form页面的构建

通常我们印象里对于表单页面一种比较常见的偏执的认知就是表单页面就是由几个控件组成的界面。但实际上表单页面应用远比这个要复杂一些。让我们设身处地想象一下小白图书馆的图书信息录入表单页面。首先,我们确实需要一个界面,界面上需要图书信息的录入控件,例如书名,出版社,第一作者等。但是这样就够了么?不够,这里有两大小方面不够。小的方面的不够:界面还缺少其他几个必要的元素。例如表单的标题还有至少一个提交按钮。又例如错误提示信息用来提示我们界面上输入的错误信息以及如何改正。而大的方面不够:缺少必要的交  互逻辑以及数据处理过程。毕竟表单页面不是只是用来看的。小白图书管的图书信息录入表单页面,在用户填写图书信息提交后,首先需要验证信息的合法性和有效性,然后需要将信息提交保存起来。所以通过以上脉络我们可以其实将Form页面的构建简单为4个部分:

  1. 表单界面
  2. 表单数据
  3. 表单交互的状态
  4. 页面交互逻辑

如果用更加专业的术语概括就是MVVM即Model-View-ViewModel,如果代入到Android开发中则对应是Model-Layout-ViewModel+Activity

表单界面Layout

首先我们需要一张白纸来描绘我们的界面,大致的样子如下图1所以的样子的表单页面:

                               

图1 表单界面草图

表单界面细致的可以分为几个部分:表单标题,表单项,表单项错误提示,表单错误提示,保存按钮。此外还会有一些例如表单用途简略描述等其他辅助要素。

首先我们需要新建一个Activity,暂时命名为FormActivity,找到其对应的布局文件activity_form.xml文件。这个布局文件就是对应的MVVM中的View。在布局文件中我们可以设计现实最基本的页面展示元素,如下图2所示:

                                    

图2 图书信息录入表单页面

正如上图中所示,图书信息录入表单页面有一个“新图书”的标题,还有五个包含“书名”,“是否租借”,“出版社”等表单项,以及一个可见的“保存”按钮。但是实际上还有五个表单项对应的错误信息提示项。默认情况下对应的错误信息提示项是不显示的,除非保存按钮被电击触发错误验证逻辑后才有可能显示。

添加表单标题

Android原生开发中,我们通常把标题设置在Activity的ActionBar中。这里我们也一样,你可以使用默认的ActionBar设置。这里我们通过自定义的Toolbar来实现。这种方式更佳的灵活,可以设置复杂而炫酷的页面头部以及一些通用的操作入口例如刷新或者返回等。使用自定义Toolbar,我们必须辅助使用AppbarLayout布局控件,具体步骤如下:

  1. 在build.gradle中引入 ‘com.google.android.material:material:1.2.0-beta01' Material Android包

  2. 在AndroidManifest.xml设置application的theme为NoActionBar,推荐使用Theme.AppCompat.Light.NoActionBar

  3. 在activity_form.xml布局文件添加Toolbar并设置对应的标题:

    <com.google.android.material.appbar.AppBarLayout android:id="@+id/appbar" android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="parent"> <androidx.appcompat.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="?attr/actionBarSize" app:title="@string/form_title"/> </com.google.android.material.appbar.AppBarLayout>

添加表单项及其错误提示

对于表单项以及错误提示的实现就是对前几张提到的一些控件和布局控件的一个综合性的一个应用,对于布局控件这里不具体展开,在小白Android原生开发系列以后章节再做具体展开。通常而言使用LinearLayout布局控件两层嵌套基本就可以解决表单布局的问题。

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:layout_constraintTop_toBottomOf="@id/appbar"
    android:orientation="vertical"
    style="@style/form_body">
    <LinearLayout
        android:layout_width="match_parent"
        android:orientation="horizontal"
        style="@style/form_line">
        <TextView 
           style="@style/form_label"
            android:layout_height="wrap_content"
            android:text="@string/form_book_name" />
        <EditText
            android:id="@+id/input_book_name"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="2" />
    </LinearLayout>
    <TextView
        android:id="@+id/error_book_name"
        android:layout_width="match_parent" 
       android:layout_height="wrap_content"
        style="@style/form_error"
        android:visibility="gone"/>
    <Switch
        android:id="@+id/input_rented"
        android:text="@string/form_rented"
        android:layout_width="match_parent"
        style="@style/form_switch"/>
    <TextView
        android:id="@+id/error_rented"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        style="@style/form_error"
        android:visibility="gone"/>
    <LinearLayout
        android:layout_width="match_parent"
        android:orientation="horizontal"
        style="@style/form_line">
        <TextView 
           style="@style/form_label"
            android:layout_height="wrap_content"
            android:text="@string/form_publisher" />
        <EditText
            android:id="@+id/input_publisher"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="2" />
    </LinearLayout>
    <TextView
        android:id="@+id/error_publisher"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" 
       style="@style/form_error"
        android:visibility="gone"/>
    <LinearLayout
        android:layout_width="match_parent"
        android:orientation="horizontal"
        style="@style/form_line">
        <TextView
            style="@style/form_label"
            android:layout_height="wrap_content"
            android:text="@string/form_first_author" />
        <EditText
            android:id="@+id/input_first_author"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="2" />
    </LinearLayout>
    <TextView
        android:id="@+id/error_first_author"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        style="@style/form_error"
        android:visibility="gone"/>
    <LinearLayout
        android:layout_width="match_parent"
        android:orientation="horizontal"
        style="@style/form_line">
        <TextView
            style="@style/form_label"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/form_place" />
        <Spinner
            android:id="@+id/input_place"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:entries="@array/place_options"
            android:layout_weight="2"/>
    </LinearLayout>
    <TextView
        android:id="@+id/error_place"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        style="@style/form_error"
        android:visibility="gone"/>
    <Button
        android:id="@+id/submit_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/save_button_text"
        style="@style/saveButton"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="@dimen/big_spacing"/>
</LinearLayout>

如上述代码所示,通常需要一个布局容器圈定所有表单项的展示区域,同时便于控制上下边距和左右留白空间,使得整体上对齐一致。小白这里配置了form_body的style来表示这个区域:

<style name="form_body">
    <item name="android:layout_marginTop">@dimen/default_spacing</item>
    <item name="android:layout_marginLeft">@dimen/small_spacing</item>
    <item name="android:layout_marginRight">@dimen/small_spacing</item>
</style>

另外嵌套使用另一个布局容器来设置每一行表单项的布局,例如书名的表单项,包含了“书名”的TextView和一个EditText输入框。所以小白这里使用LinearLayout的水平布局模式,并赋予LinearLayout固定的高度,使得每行表单项有比较统一的布局和空间排列。使用form_line的style来表示这个区域:

<style name="form_line">
    <item name="android:layout_height">36dp</item>
    <item name="android:gravity">center_vertical</item>
</style>

最后添加表单项标签文本和表单项输入本文。通常小白会给表单项标签文本框一个固定的宽度,然后给表单项输入本文框一个layout_weight. 这样所有表单项标签文本可以垂直对齐,整体布局显得更加整齐而不凌乱。当然不要忘了我们的错误提示,我们只需要另起一行,使用TextView即可,不需要布局容器控价的辅助。

添加提交按钮

最后的最后我们还需要一个提交按钮来保存界面上用户输入的数据。小白将其和表单项一起放置在form_body中,代码如下:

<Button
    android:id="@+id/submit_button"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/save_button_text"
    style="@style/saveButton"
    android:layout_gravity="center_horizontal"
    android:layout_marginTop="@dimen/big_spacing"/>

表单数据

表单数据通常指MVVM中的Model。但是对Model的定义,其实在行业里还是有争议的。有两种方面解释:一种model只是代表有用的需要存储的业务数据,另一种model不仅包含数据而且还包含交互过程中界面的一些状态。而这里表单数据特指第一种。小白将表单数据和交互的状态数据分离节藕,一是方便独立维护表单数据,不会形成互相干扰。二是通常业务数据会携带一些隐藏数据,比如版本控制信息等,这些信息会很容易和交互的状态数据混淆,独立的表单数据,更加安全,不容易造成数据错误,也方便提交和保存尤其是数据持久化。所以小白在这里只需要定义一个Book的数据实体类:

public class Book {
        private String name;
        private Boolean rented = false;
        private String publisher;
        private String firstAuthor;
        private String place;

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Boolean getRented() {
        return rented;
    }
    public void setRented(Boolean rented) {
        this.rented = rented;
    }
    public String getPublisher() {
        return publisher;
    }
    public void setPublisher(String publisher) {
        this.publisher = publisher;
    }
    public String getFirstAuthor() {
        return firstAuthor;
    }
    public void setFirstAuthor(String firstAuthor) {
        this.firstAuthor = firstAuthor;
    }
    public String getPlace() {
        return place;
    }
    public void setPlace(String place) {
        this.place = place;
    }}

表单交互状态

表单交互状态也是一种数据,用于保存某一时刻界面上除业务数据以外的界面状态描述的元数据。正如在表单数据中所说的,表单交互状态数据究竟归为Model还是ViewModel是有争议的。但是无论归结为哪个部分,小白始终觉得把它独立出来肯定是没有问题的。对于表单而言,最常见的一种状态数据就是错误验证数据。本实例中,小白需要定义个FormState类用来存储表单以及每个表单项的验证信息,代码如下:

public class FormState {
    private Book data = new Book();
    private boolean validOfName = true;
    private String errorMsgOfName="";
    private boolean validOfRented = true;
    private String errorMsgOfRent="";
    private boolean validOfPublisher=true;
    private String errorMsgOfPublisher="";
    private boolean validOfFirstAuthor=true;
    private String errorMsgOfFirstAuthor="";
    private boolean validOfPlace=true;
    private String errorMsgOfFirstPlace="";
    private boolean validOfForm=true;
    private String errorMsgOfForm="";
    public Book getData() {
        return data;
    }
    public void setData(Book data) {
        this.data = data;
    }
    public boolean isValidOfName() {
        return validOfName;
    }
    public void setValidOfName(boolean validOfName) {
        this.validOfName = validOfName;
    }
    public String getErrorMsgOfName() {
        return errorMsgOfName;
    }
    public void setErrorMsgOfName(String errorMsgOfName) {
        this.errorMsgOfName = errorMsgOfName;
    }
    public boolean isValidOfRented() {
        return validOfRented;
    }
    public void setValidOfRented(boolean validOfRented) {
        this.validOfRented = validOfRented;
    }
    public String getErrorMsgOfRent() {
        return errorMsgOfRent;
    } 
   public void setErrorMsgOfRent(String errorMsgOfRent) {
        this.errorMsgOfRent = errorMsgOfRent;
    }
    public boolean isValidOfPublisher() {
        return validOfPublisher;
    }
    public void setValidOfPublisher(boolean validOfPublisher) {
        this.validOfPublisher = validOfPublisher;
    }
    public String getErrorMsgOfPublisher() {
        return errorMsgOfPublisher;
    }
    public void setErrorMsgOfPublisher(String errorMsgOfPublisher) {
        this.errorMsgOfPublisher = errorMsgOfPublisher;
    }
    public boolean isValidOfFirstAuthor() {
        return validOfFirstAuthor;
    }
    public void setValidOfFirstAuthor(boolean validOfFirstAuthor) {
        this.validOfFirstAuthor = validOfFirstAuthor;
    } 
   public String getErrorMsgOfFirstAuthor() {
        return errorMsgOfFirstAuthor;
    }
    public void setErrorMsgOfFirstAuthor(String errorMsgOfFirstAuthor) {
        this.errorMsgOfFirstAuthor = errorMsgOfFirstAuthor;
    }
    public boolean isValidOfPlace() {
        return validOfPlace;
    }
    public void setValidOfPlace(boolean validOfPlace) {
        this.validOfPlace = validOfPlace;
    }
    public String getErrorMsgOfFirstPlace() {
        return errorMsgOfFirstPlace;
    }
    public void setErrorMsgOfFirstPlace(String errorMsgOfFirstPlace) {
        this.errorMsgOfFirstPlace = errorMsgOfFirstPlace;
    }
    public boolean isValidOfForm() {
        return validOfForm;
    }
    public void setValidOfForm(boolean validOfForm) {
        this.validOfForm = validOfForm;
    }
    public String getErrorMsgOfForm() {
        return errorMsgOfForm;
    }
    public void setErrorMsgOfForm(String errorMsgOfForm) {
        this.errorMsgOfForm = errorMsgOfForm;
    }
}

在交互状态数据中,我们需要定义每一个表单项的正确性状态及其对应的错误提示和一个整个表单的正确性状态及其对应的错误数据,分别以validOf和errorMsg前缀表示。

表单交互逻辑

表单交互逻辑需要借助Android框架的内在能力以及我们这里需要提及的一个新的东西ViewModel。此ViewModel非彼ViewModel,而是特指Android的lifecycle中的ViewModel接口。当然其作用也大致等同于MVVM中的ViewModel。

ViewModel主要用于负责提供和管理应用的数据包括表单交互状态数据,并提供数据操作的对应接口。相当于说,对数据进行封装,只对外暴露相应的数据操作的接口以便Activity或者Fragment等组件调用。

定义ViewModel

我们都知道界面交互通常涉及到数据和数据状态的变迁,而数据或者数据状态的变迁反过来又会影响应用的界面。这种套路很像设计模式中的观察者模式或者订阅模式,即界面订阅数据,如果数据有变化通知界面重新渲染。这种能力ViewModel本身是没有的,这个时候我们还需要lifecycle中的LiveData接口,推荐使用MutableLiveData接口实现, 而且很重要的一点就是当订阅对象是Activity等lifecycle对象时候,LiveData还会通过onCreated或者onResumed去触发数据订阅,非常方便。这个时候我们需要将订阅的数据用LiveData包裹,并通过ViewModel实例暴露LiveData访问接口给外部,实例代码如下:

import android.app.Application;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;

public class FormViewModel extends AndroidViewModel {
    private MutableLiveData<FormState> formStateLiveData = new MutableLiveData<FormState>();

    public FormViewModel(@NonNull Application application) {
        super(application);
        this.initialize();
    }

    private void initialize(){
        this.formStateLiveData.setValue(new FormState());
    }

    public LiveData<FormState> getFormStateLiveData(){
        return this.formStateLiveData;
    }
}

实现数据绑定

要实现数据绑定我们首先需要在FormActivity类的onCreate事件中初始化表单项以及错误提示控件实例. 然后初始化FormViewModel实例,并通过该实例的getFormStateLiveData获取数据订阅实例。 通过获取到的FormState的LiveData实例的observe方法注册数据订阅处理逻辑。具体代码如下:

public class FormActivity extends AppCompatActivity {
    private EditText inputBookName;

    private TextView textBookNameError;

    private Switch inputRented;

    private TextView textRentedError;

    private EditText inputPublisher;

    private TextView textPublisherError;

    private EditText inputFirstAuthor;

    private TextView textFirstAuthorError;

    private Spinner inputPlace;

    private TextView textPlaceError;

    private FormViewModel formViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_form);

        this.initBookName();
        this.initBookNameError();
        this.initRented();
        this.initRentedError();
        this.initPublisher();
        this.initPublisherError();
        this.initFirstAuthor();
        this.initFirstAuthorError();
        this.initPlace();
        this.initPlaceError();
        this.initFormViewModel();
    }

    private void initBookName(){
        this.inputBookName = findViewById(R.id.input_book_name);
    }

    private void initBookNameError(){
        this.textBookNameError = findViewById(R.id.error_book_name);
    }

    private void initRented(){
        this.inputRented = findViewById(R.id.input_rented);
    }

    private void initRentedError(){
        this.textRentedError = findViewById(R.id.error_rented);
    }

    private void initPublisher(){
       this.inputPublisher = findViewById(R.id.input_publisher);
    }

    private void initPublisherError(){
        this.textPublisherError = findViewById(R.id.error_publisher);
    }

    private void initFirstAuthor(){
        this.inputFirstAuthor= findViewById(R.id.input_first_author);
    }

    private void initFirstAuthorError(){
        this.textFirstAuthorError = findViewById(R.id.error_first_author);
    }

    private void initPlace(){
        this.inputPlace = findViewById(R.id.input_place);
    }

    private void initPlaceError(){
        this.textPlaceError = findViewById(R.id.error_place);
    }

    private void renderBookName(Book book){
        this.inputBookName.setText(book.getName());
    }

    private void renderBookNameError(FormState formState){
        this.textBookNameError.setVisibility(View.GONE);
        if(!formState.isValidOfName()){
            this.textBookNameError.setText(formState.getErrorMsgOfName());
            this.textBookNameError.setVisibility(View.VISIBLE);
        }
    }

    private void renderRented(Book book){
        this.inputRented.setChecked(book.getRented());
    }

    private void renderRentedError(FormState formState){ 
       this.textRentedError.setVisibility(View.GONE);
        if(!formState.isValidOfRented()){
            this.textRentedError.setText(formState.getErrorMsgOfRent());
            this.textRentedError.setVisibility(View.VISIBLE);
        }
    }

    private void renderPublisher(Book book){
        this.inputPublisher.setText(book.getPublisher());
    }

    private void renderPublisherError(FormState formState){
        this.textPublisherError.setVisibility(View.GONE);
        if(!formState.isValidOfPublisher()){
            this.textPublisherError.setText(formState.getErrorMsgOfPublisher());
            this.textPublisherError.setVisibility(View.VISIBLE);
        }
    }

    private void renderFirstAuthor(Book book){
        this.inputFirstAuthor.setText(book.getFirstAuthor());
    }

    private void renderFirstAuthorError(FormState formState){
        this.textFirstAuthorError.setVisibility(View.GONE);
        if(!formState.isValidOfFirstAuthor()){
            this.textFirstAuthorError.setText(formState.getErrorMsgOfFirstAuthor());
            this.textFirstAuthorError.setVisibility(View.VISIBLE);
        }
    }

    private void renderPlace(Book book){
        if(book.getPlace() != null){
            String[] placeOptions = getResources().getStringArray(R.array.place_options);
            int position = Arrays.asList(placeOptions).indexOf(book.getPlace());
            this.inputPlace.setSelection(position);
        }
    }

    private void renderPlaceError(FormState formState){
        this.textPlaceError.setVisibility(View.GONE);
        if(!formState.isValidOfPlace()){
            this.textPlaceError.setText(formState.getErrorMsgOfPlace());
            this.textPlaceError.setVisibility(View.VISIBLE);
        }
    }

    private void initFormViewModel(){
        this.formViewModel = new ViewModelProvider(this, ViewModelProvider.AndroidViewModelFactory.getInstance(this.getApplication())).get(FormViewModel.class);
        this.formViewModel.getFormStateLiveData().observe(this, new Observer<FormState>() {
            @Override
            public void onChanged(FormState formState) {
                Book book = formState.getData();
                renderBookName(book);
                renderBookNameError(formState);
                renderRented(book);
                renderRentedError(formState); 
                renderPublisher(book);
                renderPublisherError(formState);
                renderFirstAuthor(book);
                renderFirstAuthorError(formState);
                renderPlace(book); 

               renderPlaceError(formState);
            }
        });
    }}

在当前的实例中,小白在订阅实例中实现了数据和错误信息提示的渲染逻辑。每次FormViewModel中FormState的数据有变更,那么都会触发通过observe方法注册的数据订阅处理程序的执行,从而重新渲染页面。

实现数据验证逻辑

表单的数据验证是不可缺少的一部分,通常我们会通过自定义验证逻辑或者第三方数据验证框架来实现。那么无论怎么样,有一个道理是通用,请委托给独立的数据验证模块做处理。 小白通常会定义一个通用的表单项数据验证器,然后扩展必要的具体验证逻辑例如必要性验证或者日期格式验证等,最后通过一个组合验证器实现处理多种验证, 如下图3所示:

                                                       图3 数据验证器类图

当前实例里我们假设书名和放置位置的必填菜单项,那么我们就需要创建特定必要的验证器来处理。首先我们需要一个抽象验证器类,所有其他验证器从这个抽象类继承行为模式,代码如下:

import java.util.HashMap;
import java.util.Map;

public abstract class BaseValidator {
    protected Map<String, Object> options = new HashMap<>();
    protected String message = "";
    public final static String MESSAGE_OPTION = "errorMsg";

    public BaseValidator(){
    }

    public  BaseValidator(Map<String, Object> options){
        if(options!=null){
            this.options.putAll(options);
        }
    }

    public String getMessage(){
        return message;
    }

    public void reset(){
        message = "";
    }

    /**     * 验证     *
     * @param value
     * @return
     */
    public abstract boolean validate(Object value);
}

然后我们需要一个必要验证器RequiredValidator,代码如下:

import java.util.Map;

public class RequiredValidator extends BaseValidator {
    private final static String DEFAULT_MESSAGE = "该字段不能为空!";

    public RequiredValidator() { 
       super();
    }

    public RequiredValidator(Map<String, Object> options) {
        super(options);
    }

    private boolean isEmpty(Object value) {
        if (value != null) {
            if (value instanceof String) { 
               return ((String) value).equals("");
            }            return false;
        }
        return true;
    }

    @Override
    public boolean validate(Object value) {
        this.reset();
        if (this.isEmpty(value)) {
            this.message = (String) this.options.get(MESSAGE_OPTION);
            if (this.message == null) {
                this.message = DEFAULT_MESSAGE;
            }
            return false;
        }
        return true;
    }}

为了方便以后扩展组合,定一个组合验证器类ValidatorGroup,代码如下:

import java.util.List;

/** * 组合验证器 */
public class ValidatorGroup extends BaseValidator {

    private List<BaseValidator> validators;

    public ValidatorGroup(List<BaseValidator> validators) {
        super();
        this.validators = validators;
    }

    @Override    public boolean validate(Object value) {
        this.reset();

        if(this.validators != null){
            for(BaseValidator validator: validators){
                if(!validator.validate(value)){
                    this.message = validator.getMessage();
                    return false;
                }
            }
        }
        return true;
    }
}

最后我们需要扩展FormViewModel类定义菜单项类的数据验证逻辑. 当然如果有其他验证条件也可以通过ValidatorGroup进行扩展,保证了代码的灵活性,代码如下:

private BaseValidator initNameValidator(){
    List<BaseValidator> validators = new ArrayList<>(1);
    Map<String, Object> requiredValidatorOptions = new HashMap<>(1);
    requiredValidatorOptions.put(BaseValidator.MESSAGE_OPTION, "书名不能为空!");
    RequiredValidator requiredValidator = new RequiredValidator(requiredValidatorOptions);
    validators.add(requiredValidator);
    return new ValidatorGroup(validators);
}

private BaseValidator initPlaceValidator(){
    List<BaseValidator> validators = new ArrayList<>(1);
    Map<String, Object> requiredValidatorOptions = new HashMap<>(1);
    requiredValidatorOptions.put(BaseValidator.MESSAGE_OPTION, "放置位置不能为空!");
    RequiredValidator requiredValidator = new RequiredValidator(requiredValidatorOptions);
    validators.add(requiredValidator);
    return new ValidatorGroup(validators);
}

private boolean validate(Book book){
    boolean result = true;
    FormState formState = this.formStateLiveData.getValue();
    BaseValidator validator = this.initNameValidator();
    formState.setValidOfName(validator.validate(book.getName()));
    formState.setErrorMsgOfName(validator.getMessage());
    result = formState.isValidOfName() && result;
    validator = this.initPlaceValidator();
    formState.setValidOfPlace(validator.validate(book.getPlace()));
    formState.setErrorMsgOfPlace(validator.getMessage());
    result = formState.isValidOfPlace() && result;
    return result;
}

实现表单保存逻辑

最后我们还需要提供表单的提交保存逻辑。首先我们在FormViewModel实现数据提交保存的访问接口submit方法,代码如下:

public void sumbit(Book newBook){
    if(this.validate(newBook)){
        // to save data
    }
    FormState formState = this.formStateLiveData.getValue();
    Book book = formState.getData();
    book.setName(newBook.getName());
    book.setFirstAuthor(newBook.getFirstAuthor());
    book.setPlace(newBook.getPlace());
    book.setPublisher(newBook.getPublisher());
    book.setRented(newBook.getRented());
    this.formStateLiveData.setValue(formState);
}

然后我们仅需要在提交按钮的点击事件中调用FormViewModel的submit方法就可以了:

private void initSubmitButton(){
    this.submitButton = findViewById(R.id.submit_button);
    this.submitButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            Editable name = inputBookName.getText();
            boolean rented = inputRented.isChecked();
            Editable publisher = inputPublisher.getText();
            Editable firstAuthor = inputFirstAuthor.getText();
            Object  place =inputPlace.getSelectedItem();
            Book newBook = new Book();
            newBook.setName(name != null? name.toString(): null);
            newBook.setRented(rented);
            newBook.setPublisher(publisher!=null? publisher.toString(): null);
            newBook.setFirstAuthor(firstAuthor!=null?firstAuthor.toString():null);
            newBook.setPlace(place!=null?place.toString(): null);
            
            formViewModel.sumbit(newBook);
        }
    });
}

结论

到这里表单实例基本就结束,那么首先我们还是需要验证一下我们的程序是否有问题,先跑起来,然后什么也不填直接提交,我们看一下效果会怎么样:

                              

然后我们填报所有数据在提交一下看一下效果:

                           

貌似最后文本框显示还是有些问题,不过主体功能还是正确的,还需要做一些微调。那么这一期的安卓原生开发就讲解到这里了。