Android MVVM架构模式 详解和综合运用(一)

Android App架构设计

Android App架构设计的目的是通过设计使程序模块化,做到模块内部的高聚合和模块之间的低耦合。这样做的好处是使得程序在开发的过程中,开发人员只需要专注于一点,提高程序开发的效率,并且更容易进行后续的测试以及定位问题。但设计不能违背目的,对于不同量级的工程,具体架构的实现方式必然是不同的,切忌犯为了设计而设计,为了架构而架构的毛病。
Android的架构设计从早期的MVC模式渐渐演变出了MVP模式,分离了Model业务逻辑层和View视图层,到后来又演变出了MVVM模式,由MVP进化而来,其中VM代表ViewModel,替代了MVP模式中的Preseter中间层。

下面是这3个架构模式的具体框架:
MVC:
Android MVVM架构模式 详解和综合运用(一)_第1张图片
MVP:
Android MVVM架构模式 详解和综合运用(一)_第2张图片

MVVM:
Android MVVM架构模式 详解和综合运用(一)_第3张图片

MVC->MVP->MVVM这几个软件设计模式是一步步演化发展的,MVP模式中View视图和Model业务逻辑之间通过Presenter来中转,虽然这样隔离了Model和View,方便了测试,但是需要编写的接口和代码量很多,代码不够优雅简洁,所以MVVM弥补了这个缺陷。在MVVM使用了DataBinding即数据绑定来代替Presenter,实现View和Model之间的通信。
MVVM架构模式主要由View,Model,ViewModel三种主要的成分组成:

  • Model:数据对象,负责存储,检索和操纵数据。同时提供本应用外部对应用程序数据的操作的接口,也可以在数据变化时发出变更通知。Model不依赖于View的实现,只要外部程序调用Model的接口就能够实现对数据的增删查改。
  • View:视图对象,UI层负责绘制UI元素,提供与用户进行交互的操作功能,包括UI展现代码以及一些相关的界面逻辑代码。
  • ViewModel:数据绑定层,这里的Model指的是View的Model,跟MVVM中的Model层不是同一个对象。所谓View的Model就是一个包含View的数据属性和操作View数据的方法的对象。这种模式的关键技术就是数据绑定(DataBinding),View的变化会直接影响ViewModel,ViewModel的变化或者内容会直接体现在View上。这种架构模式实际上是框架替开发者封装了一些对象和操作,开发者只需要较少的代码就能实现比较复杂的交互。
  • Data Binding库配置

    Google大佬给开发者提供了DataBinding库来使数据绑定具有更高的灵活性和兼容性,最低支持Android 2.1(API level 7+)。使用数据绑定库在配置上有一点要求,不支持Eclipse,Android Studio版本要求1.3以上的版本,Android Gradile构建工具版本需要1.5.0-alpha1以上,目前最新是3.4,建议上Gradle官网下载后再去配置,官方网址:https://gradle.org/install。
    配置完开发工具后,还需要在Android Sdk Manager中下载最新的支持库Support repository。下载完后在当前app module项目下的build.gradle中android节点添加如下代码开启数据绑定,例如:

    android {
        ....
        dataBinding {
            enabled = true
        }
    }

    注意:如果你的app依赖一个使用数据绑定的外部模块,那么你的app也必须在build.gradle中开启数据绑定。

    Layout布局中获取数据变量

    当使用了DataBinding数据绑定后,编写XML布局文件的方式就与以前完全不一样了。XML布局文件中的根节点是layout,里面包含data节点和view根元素节点,其中data节点主要是用来声明在xml布局中需要使用的变量,view根元素节点就是以前xml布局文件中的根节点了,一般是ViewGroup类型,如LinearLayout,RelativeLayout等。
    首先先创建一个JavaBeans 对象User:

    public class User {
       private final String firstName;
       private final String lastName;
       public User(String firstName, String lastName) {
           this.firstName = firstName;
           this.lastName = lastName;
       }
       public String getFirstName() {
           return this.firstName;
       }
       public String getLastName() {
           return this.lastName;
       }
    }

    然后编写一个新的xml布局文件activity_main.xml

    
    <layout xmlns:android="http://schemas.android.com/apk/res/android">
       <data>
           <variable name="user" type="com.example.User"/>
       data>
       <LinearLayout
           android:orientation="vertical"
           android:layout_width="match_parent"
           android:layout_height="match_parent">
           <TextView android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:text="@{user.firstName}"/>
           <TextView android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:text="@{user.lastName}"/>
       LinearLayout>
    layout>

    在上面的代码中,data节点中包含了一个名为user的变量,它引用了User对象,接下来可以在元素节点属性中使用这个变量了,例如在TextView中的text属性参数使用了@{user.firstName}来获取user对象的firstName的值。这样在TextView初始化的时候就会获取user.firstName的值来设置它的text属性了,不用我们去设置setText了,这就是数据绑定了。

    注意:在User对象中firstName和lastName属性虽然是private私有类型的,但是DataBinding语法中定义@{user.firstName} 和@{user.getFirstName()} 这两种情况是等价的,也就是使用这两个语法中的其中一个就能正常工作,首选是使用第二个语法,也就是user.getFirstName(),符合JavaBean的规范。

    Activity文件中绑定数据变量

    在开始编写activity前,首先来了解下DataBinding的特性。当开启了DataBinding后,每当我们新创建一个layout布局文件时,构建工具都会自动生成一个与这个layout名字相关的Binding类。构建工具会把你的layout文件名转换为驼峰式大小写形式,后面再添加Binding后缀,例如在上面创建的activity_main.xml,系统就会生成一个名为ActivityMainBinding的类。这些生成Binding类就是DataBinding数据绑定的库的核心所在了,这些Binding类包含了在layout文件中声明的变量,例如上面的user变量,也就是在Binding类中也会有setUser和getUser方法,并且这些类会自动解析view元素属性中的binding表达式@{ expressions },然后自动把值设置给这个属性。
    可以在build-generated-source-apt-debug-com文件夹中看到这些生成的Binding源码:
    Android MVVM架构模式 详解和综合运用(一)_第4张图片

    private ActivityMainBinding binding;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       binding= DataBindingUtil.setContentView(this,R.layout.activity_main);
       User user = new User("Test", "User");
       binding.setUser(user);
    }

    运行后,layout中的textView的text被设置为Test和User了,不用我们去setText方法了,降低耦合,减少了代码的输入流,实现数据绑定。
    官方还有一种方式,例如下面代码,但是运行没效果,求评论指点:

    ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());

    如果在ListView或者RecyclerView中使用adapter,那么就要使用data binding items了,例如下面的代码:

    ListItemBinding binding = ListItemBinding.inflate(layoutInflater, viewGroup, false);
    //or
    ListItemBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.list_item, viewGroup, false);

    Layout布局文件中新增属性和元素标记

  • Import元素标记
    import关键字在java文件中很常见,一般是用来导进外部class文件的,在xml布局文件中的元素标记里面可以使用元素标记来引用外部的class,导进了外部的class文件后,就可以在xml布局文件中使用了,注意需要使用全限定名:
    例如之前是使用type来引用外部class,来声明变量的类型:
  •    <data>
           "user" type="com.example.User"/>
       data>

    现在可以使用import元素了:

    <data>
        <import type="com.example.User"/>
        <variable
            name="user"
            type="User"/>//这样就不用输入全路径名了
    </data>

    使用import元素的另一个用途是导进工具类,然后可以在xml布局文件中直接调用静态方法或者静态属性:

    //编写一个工具类
    public class StrUtil {
        public static String addWarming(String str){
            return str+"didi";
        }
    }
    //在布局文件中引进
        <data>
                <import type="utils.StrUtil"/>
                <variable name="user" type="com.example.User"/>
        </data>
    //然后在view的属性中使用@{ }就可以直接调用了
                <TextView
                    android:textSize="14sp"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="@{StrUtil.addWarming(user.getName())}"
                    tools:text="Jason"/>

    同时也可以导进android中的提供的内置类:

    <data>
        <import type="android.view.View"/>
    data>
    
    <TextView
       android:text="@{user.lastName}"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:visibility="@{user.isAdult ? View.VISIBLE : View.GONE}"/>

    import元素标记除了type属性,还有alias属性。alias属性是为了解决导入的类同名冲突的情况,使用这个属性就可以给类设置别名,例如下面的代码创建了一个自定义View,和系统的View同名冲突:

        <data>
            <import type="android.view.View"/>
            <import type="utils.View" alias="ViewUtil"/>

    这样在layout文件中View引用android.view.View,而ViewUtil引用utils.View自定义类。
    同时Import也可以导进数据类型,所以variable属性和@{ }表达式中使用:

    <data>
        <import type="com.example.User"/>
        <import type="java.util.List"/>
        <variable name="user" type="User"/>
        <variable name="userList" type="List<User>"/>//<是<转义,>是>转义
    </data>

  • Variables
    在layout的元素标记内可以包含N个Variables元素,Variables就是声明一个在layout文件中的binding表达式@{ } 中使用的变量:
  • <data>
        <import type="android.graphics.drawable.Drawable"/>
        <variable name="user"  type="com.example.User"/>
        <variable name="image" type="Drawable"/>
        <variable name="note"  type="String"/>
    </data>

    在构建工具自动生成的binding class中为自动会每个variables变量创建一个setter和getter方法,每个变量都会自动初始化,引用类型初始化为null,基本数据类型如int初始化为0,boolean初始化为false等。
    注意构建工具会自动为我们开发者生成一个context变量方便我们在binding表达式中使用,不用自己导进Context类,这个context的值就是view根元素的getContext( )方法中获取的Context上下文。我们可以显式声明一个Context变量来重写这个context,但一般情况下不用的。

  • 自定义binding class名
    构建工具默认生成Binding class名字是根据layout文件名来生成的,一般是以大写字母开头,替换_下划线,并在下划线之后的第一个字母转换为大写,即驼峰式大小写规则,最后加Binding后缀.class。可以在build-generated-source-apt-debug-com-example.xxxx.xxxx-databinding文件夹中看到生成的这些类。
    可以在标记中使用class属性来重命名关联当前布局的Binding类,或者重定向这个Binding类的路径,例如:
  • //会在默认路径中生成一个ContactItem的class类
    "ContactItem">
        ...
    
    //加上.前戳,会在默认包路径下生成class类
    ".ContactItem">
        ...
    
    //或者使用路径全限定名明确指定生成的路径
    "com.example.ContactItem">
        ...
    

  • Includes
    variables声明的变量可以直接传递进去在布局文件中引用的重用布局Include,这样include引用的布局文件中的声明的变量就可以使用外部layout文件中的命名空间和变量值,前提是include布局中必须拥有和外部layout同名的variable属性:
  • //在name.xml和contact.xml中必须声明user变量
    
    <layout xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:bind="http://schemas.android.com/apk/res-auto">
       <data>
           <variable name="user" type="com.example.User"/>
       data>
       <LinearLayout
           android:orientation="vertical"
           android:layout_width="match_parent"
           android:layout_height="match_parent">
           <include layout="@layout/name"
               bind:user="@{user}"/>
           <include layout="@layout/contact"
               bind:user="@{user}"/>
       LinearLayout>
    layout>

  • Layout中@{ }使用的表达式
    在@{ }绑定语句中可以使用类似在Java 表达式中使用一些语法,例如:
    计算表达式 + - / * %
    字符串连接符 +
    逻辑运算符 && ||
    二进制运算符 & | ^
    一元运算符 + - ! ~
    位移运算符 >> >>> <<
    比较运算符 == > < >= <=
    判断类型符 instanceof
    分组 Grouping ()
    字面量符号 Literals - character, String, numeric, null
    强制类型转换 Cast
    方法调用 Method calls
    访问属性 Field access
    使用数组 Array access []
    三元运算符 Ternary operator ?:
    例如:
  • android:text="@{String.valueOf(index + 1)}"
    android:visibility="@{age < 13 ? View.GONE : View.VISIBLE}"
    android:transitionName='@{"image_" + id}'

    注意:当在@{ } 表达式使用双引用后,在外部可以使用单引用代替双引号,例如’@{“image_” + id}’。

  • 空合并操作符 ??
    空合并操作符?? 是新增的运算符,类似三元运算符? : ; ,如果参数是null,就使用左边的表达式,如果不是null,就使用右边的表达式:
  • android:text="@{user.displayName ?? user.lastName}"
    android:text="@{user.displayName != null ? user.displayName : user.lastName}"
    //两者是等价的

  • 避免空指针
    自动生成的Binding类代码会自动检查变量是否为空以避免发生空指针异常。例如,在表达式@{user.name}中,如果user变量我们没有初始化,那么user就是Null,user.name的值就会被赋予默认值null。基本数据类型同样会被赋予默认值。
  • layout布局文件中使用集合
    DataBinding框架支持Java中的集合类型,例如数组,List,SparseList和Map,可以使用[ ]操作符来使用它们。
  • <data>
        <import type="android.util.SparseArray"/>
        <import type="java.util.Map"/>
        <import type="java.util.List"/>
        <variable name="list" type="List<String>"/>
        <variable name="sparse" type="SparseArray<String>"/>
        <variable name="map" type="Map<String, String>"/>
        <variable name="index" type="int"/>
        <variable name="key" type="String"/>
    </data>
    …
    android:text="@{list[index]}"
    …
    android:text="@{sparse[index]}"
    …
    android:text="@{map[key]}"

  • 字符串字面量双引号和单引号的使用
    但在@{ } 表达式中使用双引号时,可以在使用单引号包围属性值。或者使用双引号包围属性值,@{ }表达式中使用单引号`或者’。这两种方法是等价的。
  • android:text='@{map["firstName"]}'
    
    android:text="@{map[`firstName`}"
    android:text="@{map['firstName']}"

  • 访问资源
    在layout布局文件元素属性值中可以使用正常的访问资源的表达式语法:
  • android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}"

    一些资源需要显式资源的引用:

    数据类型 原生引用 @{}表达式中引用
    String[] @array @stringArray
    int[] @array @intArray
    TypedArray @array @typedArray
    Animator @animator @animator
    StateListAnimator @animator @stateListAnimator
    color int @color @color
    ColorStateList @color @colorStateList

    你可能感兴趣的:(Android)