美文网首页Android架构Android进阶Android
一篇文章带你入门 Android MVVM

一篇文章带你入门 Android MVVM

作者: 接地气的二呆 | 来源:发表于2016-03-10 22:19 被阅读6879次

    我的更多 android 博文,关注作者~每周更新一篇 Android干货博文
    http://xuyushi.github.io/archives/

    在我的上一篇博文中介绍了Android的MVP模式(http://www.jianshu.com/p/f6252719b3af) MVVM 是从 MVP 的进一步发展与规范,MVP 隔离了 M 与 V 的直接联系后,靠 Presenter 来中转,所以使用 MVP 时 P 是直接调用 View 的接口来实现对视图的操作的,M 与 V是隔离了,方便测试了,但代码还不够优雅简洁啊,所以 MVVM 就弥补了这些缺陷。

    概述

    MVVM模式包含了三个部分:

    • Model :基本业务逻辑
    • View :视图内容
      ViewModel: 将前面两者联系在一起的对象

    当View有用户输入后,ViewModel通知Model更新数据,同理Model数据更新后,ViewModel通知View更新。

    MVP MVVM区别

    可以看到 ViewModel 承担了 Presenter 中与 view和 Model 交互的职责,与 MVP模式不同的是,VM与 V 之间是通过 Datebingding 实现的,而 P是持有 View 的对象,直接调用 View 中的一些接口方法来实现。ViewModel可以理解成是View的数据模型和Presenter的合体。**它通过双向绑定(松耦合)解决了MVP中Presenter与View联系比较紧密的问题。 **

    环境搭建

    Android 的 Gradle 插件版本不低于 1.5.0-alpha1:

    classpath 'com.android.tools.build:gradle:1.5.0'
    

    然后修改对应模块(Module)的 build.gradle:

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

    注:Android stuido 的版本要大于1.3
    Android Studio目前对binding对象没有自动代码提示,只会在编译时进行检查。

    基础入门

    布局问文件

    相比传统的 xml,根节点编程了layout,里面包括了data节点 和传统的视图。data节点就像是连接 View 和 Modle 的桥梁。在data节点中声明一个variable变量,使其可以在这个layout中使用

    <layout xmlns:android="http://schemas.android.com/apk/res/android">
        <data>
            <variable name="user"type="com.example.User"/>
        </data>
        <!--原先的根节点(Root Element)-->
        <LinearLayout>
        ....
        </LinearLayout>
    </layout>
    

    例如在 TextView 中使用

    <TextView android:layout_width="wrap_content"
    android:layout_height="wrap_content"          android:text="@{user.firstName}"/>
    

    数据对象

    定义一个 User java bean 类

    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 firstName;
        }
    
        public String getLastName() {
            return lastName;
        }
    }
    

    layout中定义User中的对象,然后把它跟布局文件中声明的变量进行绑定

    定义 Variable

        <data>
    
            <variable
                name="user"
                type="io.github.xuyushi.androidmvvmdemo.User" />
        </data>
    
    • 变量名为user
    • 变量类型为"io.github.xuyushi.androidmvvmdemo.User"

    data也支持 import

    <data>
       <import type="io.github.xuyushi.androidmvvmdemo.User"/>
       <variable
           name="user"
           type="User" />
    </data>
    

    注意坑
    import 并不能和 java 一样可以 import xx.xxx.*,必须具体写明每个要导入的类名,如

    <import type="io.github.xuyushi.androidmvvmdemo.User"/>
    <import type="io.github.xuyushi.androidmvvmdemo.MyHandler"/>
    
    
    // this is WRONG
    <import type="io.github.xuyushi.androidmvvmdemo.*"/>
    

    编译之后,插件会根据 xml 的命名(activity_main),在 output会生成ActivityMainBinding

    java.lang.* 包中的类会被自动导入,可以直接使用,例如要定义一个 String 类型的变量:
    <variable name="firstName" type="String" />

    绑定 Variable

    修改MainActivity中的onCreate,用 DatabindingUtil.setContentView() 来替换掉 setContentView(),然后创建一个 user 对象,通过 binding.setUser(user) 与 variable 进行绑定。

    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
            User user = new User("testFirst", "testLast");
            binding.setUser(user);
        }
    }
    

    如果使用的 ListView 或者RecyclerView可以使用这个

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

    有时候不能预先知道 Bingding 类的种类,这时候可以使用DataBindingUtil 类:

    ViewDataBinding binding = DataBindingUtil.inflate(LayoutInflater, layoutId,
        parent, attachToParent);
    ViewDataBinding binding = DataBindingUtil.bindTo(viewRoot, layoutId);
    

    支持的语法

    Mathematical + - / * %
    String concatenation +
    Logical && ||
    Binary & | ^
    Unary + - ! ~
    Shift >> >>> <<
    Comparison == > < >= <=
    instanceof
    Grouping ()
    Literals - character, String, numeric, null
    Cast
    Method calls
    Field access
    Array access []
    Ternary operator ?:
    
    

    不支持的语法

    this
    super
    new
    
    

    ActivityMainBinding类是自动生成的,所有的 set 方法也是根据 variable 名称生成的。例如,我们定义了两个变量。

    <data>
        <variable name="firstName" type="String" />
        <variable name="lastName" type="String" />
    </data>
    

    那么会生成两个 set方法

    setFirstName(String firstName);
    setLastName(String lastName);
    

    使用Variable

    数据与 Variable 绑定之后,xml 的 UI 元素就可以直接使用了

    <TextView
      android:text="@{user.firstName}"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content" />
    
    <TextView
      android:text="@{user.lastName}"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content" />
    

    绑定事件

    可以直接在 xml 导入android.view.View.OnClickListener,并制定其点击事件

    <variable
      name="clickListener"
      type="android.view.View.OnClickListener" />
      
     ...
     android:onClick="@{clickListener}"
     ...
    
     holder.binding.setClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                //do something
            });
    

    进阶用法

    使用类的方法

    //Error:(27, 29) cannot find method addSomeThing in class io.github.xuyushi.androidmvvmdemo.MyUtill

    类的别名

    如果导入不同的包中有相同的类名,使用import 中的 alias 属性。

    <import type="com.example.home.data.User" />
    <import type="com.examle.detail.data.User" alias="DetailUser" />
    <variable name="user" type="DetailUser" />
    

    数据绑定

    直接修改数据对象并不能直接更新 UI,Android的Data Binding模块给提供了通知机制,有3种类型,分别对应于类(Observable),字段(ObservableField),集合类型(Observable Collections)。

    Android的Data Binding模块给提供了通知机制,有3种类型,分别对应于类(Observable),字段(ObservableField),集合类型(Observable Collections)。

    Observable Objects

    目前 DataBinding 暂时只支持单向绑定。

    要实现 Observable Binding,首先得有一个 implement 了接口android.databinding.Observable的类,为了方便,Android 原生提供了已经封装好的一个类 - BaseObservable,并且实现了监听器的注册机制

    public class User extends BaseObservable {
        private String firstName;
        private String lastName;
        @Bindable
        public String getFirstName() {
            return this.firstName;
        }
        @Bindable
        public String getLastName() {
            return this.lastName;
        }
        public void setFirstName(String firstName) {
            this.firstName = firstName;
            notifyPropertyChanged(io.github.xuyushi.androidmvvmdemo.BR.firstName);
        }
        public void setLastName(String lastName) {
            this.lastName = lastName;
            notifyPropertyChanged(io.github.xuyushi.androidmvvmdemo.BR.lastName);
        }
    }
    

    The Bindable annotation should be applied to any getter accessor method of an {@link Observable} class. Bindable will generate a field in the BR class to identify the field that has changed.

    Bindable注解是为了在编程的时候生成 BR 类,Bindable会在 BR 类中生成一个域变量 ,来表明这个域有木有被改变。通过代码可以看出,当数据发生变化时还是需要手动发出通知。 通过调用 notifyPropertyChanged(BR.firstName) 可以通知系统 BR.firstName 这个 entry 的数据已经发生变化,需要更新 UI。

    ObservableFields

    具体到成员变量,这种方式无需继承 BaseObservable
    如果变量比较少,都是简单的数据类型是时,可以用ObservableFieldsObservableFields 自包含具有单个字段的observable对象。它有所有基本类型和一个是引用类型。要使用它需要在data对象中创建public final字段:

    private static class User extends BaseObservable {
       public final ObservableField<String> firstName =
           new ObservableField<>();
       public final ObservableField<String> lastName =
           new ObservableField<>();
       public final ObservableInt age = new ObservableInt();
    }
    

    注意

    • 可以在 java bean 中定义,也可以在 activity 中 或者bind 出定义
    • 使用ObservableFields 在 Model 中的 @Bindable get set 方法都可以去掉
    • firstNamelastName变化时,UI 会得到通知,使用的赋值语句为user.firstName.set("Google");

    Observable 集合

    一些app使用更多的动态结构来保存数据。Observable集合允许键控访问这些data对象。ObservableArrayMap用于键是引用类型,如String。

    ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
    user.put("firstName", "Google");
    user.put("lastName", "Inc.");
    user.put("age", 17);
    

    在layout文件中,通过String键可以访问map

    <data>
        <import type="android.databinding.ObservableMap"/>
        <variable name="user" type="ObservableMap<String, Object>"/>
    </data>
    …
    <TextView
       android:text='@{user["lastName"]}'
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"/>
    <TextView
       android:text='@{String.valueOf(1 + (Integer)user["age"])}'
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"/>
    

    当 key 是 inter 是, ObservableArrayList 比较适用

    ObservableArrayList<Object> user = new ObservableArrayList<>();
    user.add("Google");
    user.add("Inc.");
    user.add(17);
    
    <data>
        <import type="android.databinding.ObservableList"/>
        <import type="com.example.my.app.Fields"/>
        <variable name="user" type="ObservableList<Object>"/>
    </data>
    …
    <TextView
       android:text='@{user[Fields.LAST_NAME]}'
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"/>
    <TextView
       android:text='@{String.valueOf(1 + (Integer)user[Fields.AGE])}'
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"/>
    

    binding 生成

    binding 类连接了 layout中的variables与Views。,所生成的Binding类都扩展了android.databinding.ViewDataBinding

    创建

    Binding应在inflation之后就立马创建,以确保View层次结构没被改变。

    首先 inflate

    MyLayoutBinding binding = MyLayoutBinding.inflate(layoutInflater);
    MyLayoutBinding binding = MyLayoutBinding.inflate(LayoutInflater, viewGroup, false);
    

    带 ID 的 View

    同步 bind 我们可以不需要 view 实例,但是玩意需要也可以有

    <TextView
        android:id="@+id/firstName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    

    上面代码中定义了一个 ID 为 firstName 的 TextView,那么它对应的变量就是
    public final TextView firstName;

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            binding = DataBindingUtil.setContentView(this, R.layout.activity_view_with_ids);
        }
    
        public void showMyName(View view) {
            binding.firstName.setText("liang");
            binding.lastName.setText("fei");
        }
    

    这样就免去了些 findViewById

    ViewStubs

    ViewStubs跟正常的Views略有不同。他们开始时是不可见的,当他们要么设置为可见或被明确告知要载入时,它们通过载入另外一个layout取代了自己。

    当载入另一个layout,为新的布局必需创建一个Binding。因此,ViewStubProxy必需监听ViewStub的OnInflateListener监听器并在那个时候建立Binding。因为只有一个可以存在,ViewStubProxy允许开发者在其上设置一个OnInflateListener它会在建立Binding后调用。

    <layout xmlns:android="http://schemas.android.com/apk/res/android">
        <LinearLayout
            ...>
            <ViewStub
                android:id="@+id/view_stub"
                android:layout="@layout/view_stub"
                ... />
        </LinearLayout>
    </layout>
    

    在 Java 代码中获取 binding 实例,ViewStubProy 注册ViewStub.OnInflateListener 事件:

    binding = DataBindingUtil.setContentView(this, R.layout.activity_view_stub);
    binding.viewStub.setOnInflateListener(new ViewStub.OnInflateListener() {
        @Override
        public void onInflate(ViewStub stub, View inflated) {
            ViewStubBinding binding = DataBindingUtil.bind(inflated);
            User user = new User("fee", "lang");
            binding.setUser(user);
        }
    });
    

    动态 Variables

    有时候不止具体绑定的对象,以 RecyclerView 为例,Adapter 的 DataBinding 需要动态生成,因此我们可以在 onCreateViewHolder 的时候创建这个 DataBinding,然后在 onBindViewHolder 中获取这个 DataBinding。

    public static class BindingHolder extends RecyclerView.ViewHolder {
        private ViewDataBinding binding;
    
        public BindingHolder(View itemView) {
            super(itemView);
        }
    
        public ViewDataBinding getBinding() {
            return binding;
        }
    
        public void setBinding(ViewDataBinding binding) {
            this.binding = binding;
        }
    }
    
    @Override
    public BindingHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
        ViewDataBinding binding = DataBindingUtil.inflate(
                LayoutInflater.from(viewGroup.getContext()),
                R.layout.list_item,
                viewGroup,
                false);
        BindingHolder holder = new BindingHolder(binding.getRoot());
        holder.setBinding(binding);
        return holder;
    }
    
    @Override
    public void onBindViewHolder(BindingHolder holder, int position) {
        User user = users.get(position);
        holder.getBinding().setVariable(BR.user, user);
        holder.getBinding().executePendingBindings();
    }
    

    属性 Setter

    每当绑定值的变化,生成的Binding类必须调用setter方法​​。Data Binding框架有可以自定义赋值的方法。

    自动Setters

    对于一个属性,Data Binding试图找到setAttribute方法。与该属性的namespace并不什么关系,仅仅与属性本身名称有关

    例如,有关TextView的android:text属性的表达式会寻找一个setText(String)的方法。如果表达式中的参量是一个int,Data Binding会搜索的setText(int)方法。注意:要表达式返回正确的类型,如果需要的话使用转型。Data Binding仍会运行即使没有给定名称的属性存在。然后,您可以通过Data Binding轻松地为任何setter“创造”属性。例如,DrawerLayout没有任何属性,但可以有很多的setters。您可以使用其中的一个setters。

    <android.support.v4.widget.DrawerLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:scrimColor="@{@color/scrim}"
        app:drawerListener="@{fragment.drawerListener}"/>
    

    这里我们增加了一个命名空间app,并且注意DrawerLayoutapp:scrimColor属性,这里和我们自定义view时自定义的属性一样,但是这里并不需要我们去重写DrawerLayout,此时,我们可以自己定义setTcrimColorsetDrawerListener的方法

    重命名的Setters

    一些有setters的属性按名称并不匹配。对于这些方法,属性可以通过BindingMethods注解相关联。这必须与一个包含BindingMethod注解的类相关联,每一个用于一个重命名的方法。例如,android:tint属性与setImageTintList相关联,而不与setTint相关。

    @BindingMethods({
           @BindingMethod(type = "android.widget.ImageView",
                          attribute = "android:tint",
                          method = "setImageTintList"),
    })
    
    

    自定义Setters

    有些属性需要自定义绑定逻辑。例如,对于android:paddingLeft属性并没有相关setter。相反,setPadding(left, top, right, bottom)是存在在。一个带有BindingAdapter注解的静态绑定适配器方法允许开发者自定义setter如何对于一个属性的调用。

    Android的属性已经创造了BindingAdapters。举例来说,对于paddingLeft:

    @BindingAdapter("android:paddingLeft")
    public static void setPaddingLeft(View view, int padding) {
       view.setPadding(padding,
                       view.getPaddingTop(),
                       view.getPaddingRight(),
                       view.getPaddingBottom());
    }
    

    Binding适配器对其他定制类型非常有用。例如,自定义loader可以用来异步载入图像。
    当有冲突时,开发人员创建的Binding适配器将覆盖Data Binding默认适配器。
    您也可以创建可以接收多个参数的适配器。

    @BindingAdapter({"bind:imageUrl", "bind:error"})
    public static void loadImage(ImageView view, String url, Drawable error) {
       Picasso.with(view.getContext()).load(url).error(error).into(view);
    }
    

    loadImage 可以放在任意类中,该类中只有一个静态的方法imageLoader,该方法有3个参数,一个是需要设置数据的view, 一个是我们需要的url、有个个是错误加载的图像,值得注意的是那个BindingAdapter注解,看看他的参数,是一个数组,内容只有一个bind:imageUrl,仅仅几行代码,我们不需要 手工调用 (类 xxxxxxx)中的loadImage,也不需要知道loadImage方法定义到哪了,一个网络图片加载就搞定了这里面起关键作用的就是BindingAdapter 注解,这里要遵循一定的规则,、

    以bind:开头,接着书写你在控件中使用的自定义属性名称。

    转换器

    在 xml 中为属性赋值时,如果变量的类型与属性不一致,通过 DataBinding 可以进行转换

    例如,下面代码中如果要为属性 android:background 赋值一个 int 型的 color 变量:

    <View
        android:background="@{isError.get() ? @color/red : @color/white}"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_height="@{height}" />
    

    只需要定义一个标记了 @BindingConversion 的静态方法即可(方法的定义位置可以随意):

    @BindingConversion
    public static ColorDrawable convertColorToDrawable(int color) {
        return new ColorDrawable(color);
    }
    

    再举个栗子 ,假如你的控件需要一个格式化好的时间,但是你只有一个Date类型的变量。可以转化完成后在设置,此时更适合使用 conver

    <layout xmlns:android="http://schemas.android.com/apk/res/android">
        <data class=".Custom">
            <variable
                name="time"
                type="java.util.Date" />
        </data>
    
        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{time}"/>
    </layout>
    
    binding.setTime(new Date());
    

    看看 conver

    public class ConvertUtil {
        @BindingConversion
        public static String convertDate(Date date) {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
            return sdf.format(date);
        }
    }
    
    

    :convert 可以放在任意包中,只要写明注解,已经被转换和转换成的类型,所以注意不要重复定义类型相同的 convert,使用 Converter 一定要保证它不会影响到其他的属性。举个栗子,int -> int 的 convert 就影响到了android:visibility

    Android stuido 的预览支持

    类似于 tools:text ,代码如下

    <TextView
                style="@style/TextAppearance.AppCompat.Large"
                android:text="@{user.firstName,default=PLACEHOLDER}"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content" />
    

    举个例子

    工程分为两个部分

    • 第一个简单的例子,点击事件产生后,直接改变了,user 的数据,并没有对 view 操作的逻辑,但是 databinding 已经帮我们完成了一切
    • 第二个例子是一个 recycleVIew 的例子,点击每个 cardview 增加一点数据

    代码不贴了,放 github了
    https://github.com/xuyushi/AndroidMVVMDemo

    我的更多 android 博文
    http://xuyushi.github.io/archives/
    关注作者~每周更新一篇 Android干货博文

    参考

    https://www.zhihu.com/question/30976423
    https://developer.android.com/intl/zh-cn/tools/data-binding/guide.html#generated_binding
    https://segmentfault.com/a/1190000002876984
    http://tech.vg.no/2015/07/17/android-databinding-goodbye-presenter-hello-viewmodel/
    http://www.jianshu.com/p/4e3220a580f6
    https://github.com/LyndonChin/MasteringAndroidDataBinding
    http://www.cnblogs.com/dxy1982/p/3793895.html
    https://realm.io/cn/news/data-binding-android-boyar-mount/?utm_source=tuicool&utm_medium=referral
    https://www.aswifter.com/2015/07/04/android-data-binding-1/
    http://blog.csdn.net/qibin0506/article/details/47720125

    相关文章

      网友评论

      本文标题:一篇文章带你入门 Android MVVM

      本文链接:https://www.haomeiwen.com/subject/wqwtlttx.html