欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android开发利器之Data Binding Compiler V2 —— 搭建Android MVVM完全体的基础

程序员文章站 2022-07-03 18:43:10
原创声明: 该文章为原创文章,未经博主同意严禁转载。 前言: Android常用的架构有:MVC、MVP、MVVM,而MVVM是唯一一个官方提供支持组件的架构,我们可以通过Android lifecycle系列组件、DataBinding或者通过组合两者的形式来打造一个强大的MVVM架构。而Data ......

Android开发利器之Data Binding Compiler V2 —— 搭建Android MVVM完全体的基础

原创声明: 该文章为原创文章,未经博主同意严禁转载。

前言: android常用的架构有:mvc、mvp、mvvm,而mvvm是唯一一个官方提供支持组件的架构,我们可以通过android lifecycle系列组件、databinding或者通过组合两者的形式来打造一个强大的mvvm架构。而databinding compiler v2就是为了解决目前的mvvm架构中的缺点而诞生的。

data binding和livedata的兼容问题

在databinding compiler v1的环境下,databinding和livedata是无法兼容的。这句话是什么意思呢?我们先来看看平时我们使用databinding的代码片段。

data binding

布局代码片段

<data>  
    <variable  
        name= "text"  
        type="android.databinding.observablefield&lt;string>"/>  
</data>  
  
<textview  
    android:layoutwidth="matchparent"  
    android:layoutheight="40dp"  
    android:text=“@{text}“  
    />

注:xml不能直接使用‘<’所以我们需要使用转义符:"<"
使用代码片段

xxxbinding binding = ...  
private final observablefield<string> text = new observablefield<>();  
binding.settext(text)  
text.set(" hello word ")`

上面的代码片段是databinding的简单使用方法。

livedata

我们知道livedata是google官方推出的生命周期感知的数据包装组件,用来搭建mvvm框架有天然的优势,能很好协调控制层与展示层生命周期不一致的问题(这里是指view层与viewmodel层)下面我们来看下使用livedata更新ui的代码片段。

viewmodel代码片段

public class testmodel extends viewmodel {  
    private final mutablelivedata<string> text = new mutablelivedata<>();  
  
    public livedata<string> gettext() {  
        return text;  
    }  
}  

view层代码片段

viewmodel. gettext().observe(this, observe -> {  
    tvtext.settext(observe);  
});  

当我们在viewmodel中调用 text.postvalue(obj)方法时,ui层的observe方法就会收到回调,通过tvtext.settext(observe);这句代码来更新tvtext。

例如,我们可以在viewmodel中通过下面的代码来更新ui层

text.posvalue("hello word !")  

可以看出,无论是使用databinding还是livedata,都能实现view层和viewmodel层解耦的目的,并且能viewmodel层中的数据变化来实现view层的更新,这就是我们常说数据驱动视图

数据驱动视图:只要数据变化, 就重新渲染视图

observablefield与livedata

我们知道databinding是通过observablefield来实现数据的双向绑定的,而observablefield本质上就是一个被观察者,而我们的xml布局文件和就是观察者,当observablefield产生变化是会通知我们的布局文件更新布局(观察者模式)。
observablefield如何实现通知布局文件更新的原理我们这里先不深入讨论,这里笔者只给出一个结论,observablefield被view层(这里指我们的xml布局文件)以弱引用的方式引用,当observablefield更新时,会通过监听器通知view层,并且observablefield是对view层生命周期不敏感的。所以通过observablefield实现数据双向绑定并不是一个完美的方案。

我们可以考虑使用livedata来实现双向绑定。
我们先来回顾一下监听livedata方法:

viewmodel. gettext().observe(this, observe -> {  
    tvtext.settext(observe);  
});  

非常简单,只在调用livedata的observe,设置一个observer
回调监听器就可以了。

那么上文提到的databinding与livedata不兼容是指什么呢?
从上面的分析我们可以看出observablefield与livedata的使用方式完全是完全不一样的,observablefield可以通过直接在布局文件中设置实现双向绑定。而livedata必须通过代码设置监听器,并且需要手动调用待更新的控件才能实现控件的更新。就是说livedata只能通知ui层有数据需要更新,更新后的数据是什么,但是并不能自动帮你实现view的更新。并且当view层的数据更新后,livedata也没办法自动获取view层的更新。

例如:在使用edittext的时候,要获取edittext的改变,需要调用edittext的gettext方法,而observablefield只需要调用get()方法即可

livedata在data binding compiler v1下是无法使用类似observablefield的方式实现数据绑定的(单向也不行),这就是笔者所说的databinding与livedata不兼容。
当我们使用databinding与lifecycle组合搭建mvvm框架的时候,需要根据业务的具体需要来选择使用livedata还是observablefield。类似下面的代码:

public final observableboolean dataloading = new observableboolean(false);  
  
private final mutablelivedata<void> mtaskupdated = new mutablelivedata <>();  

但是实际开发的时候,我们往往无法在observablefield与livedata中作出很好的选择,因为它们的优缺点都太明显了。
我们总结一下observablefield与livedata的优缺点。
 observablefield
优点:使用方便,能快速实现双向绑定
缺点:使用弱引用的方式与view层,并且不能根据view层的生命周期来发送通知

livedata
优点:能根据view层的生命周期来发送通知事件
缺点:使用麻烦,与view层耦合大,并且不支持数据与view绑定

data binding compiler v2

我们要说的主角就是,data binding compiler v2 。

什么是data binding compiler呢?

data binding compiler是data binding的编译器,它的主要作用就是编译出我们在使用data binding时需要使用的辅助代码。例如:activityxxxbinding格式的类文件就是由data binding compiler编译生成的,并且observablefield数据双向绑定也是由编译器编译的代码提供支持的。
data binding compiler v2是data binding的第二代编译器,这个编译器和v1编译器最大的不同就是:v1编译器只支持observablefield系列的数据包装类与view层的双向绑定,而v2编译器能让livedata支持data binding双向绑定。
我们可以看看在v2编译器环境下livedata实现双向绑定的代码片段:
布局代码片段

<data>  
    <variable  
    name="text"  
    type="android.arch.lifecycle.livedata&lt;string>"/>  
</data>  
  
<textview  
    android:layoutwidth="matchparent"  
    android:layoutheight="40dp"  
    android:text=“@{text}“  
    />

使用代码片段

xxxbinding binding = ...  
binding.setlifecycleowner(this);  
mutablelivedata<string> text = new mutablelivedata<>();  
binding.settext(text);  
text.postvalue(" hello word ");

可以看出,在data binding compiler v2 环境下,使用livedata实现双向绑定的方法和使用observable实现双向绑定的方法基本山是一样的。通过data binding compiler v2我们能把livedata不能实现双向绑定和使用麻烦的缺点彻底解决,并且还能保留livedata能感知view层生命周期的优点保留下来。

如何使用data binding compiler v2?

环境配置

要使用data binding compiler v2 的话,可能需要升级一下开发环境,需要的配置如下。

  • android studio 版本需要升级到3.1 canary 6以上
  • gradle版本需要升级到 alpha06以上
  • gradle-wrapper.properties中的distributionurl需要改成gradle-4.4
distributionurl=https\://services.gradle.org/distributions/gradle-4.4-all.zip  
  • 需要在gradle.properties文件中启用databinding v2
android.databinding.enablev2=true  

当我们配置完后,重新clear一下项目就可以开启data binding compiler v2了。

使用方法

我们以一个模拟登陆的例子来简单介绍如何使用data binding compiler v2。

数据类

public class account {  
    private mutablelivedata<string> accountnum = new mutablelivedata<>();  
    private mutablelivedata<string> password = new mutablelivedata<>();  
  
    account(string accountnum, string password){  
        this.accountnum.setvalue(accountnum);  
        this.password.setvalue(password);  
    }  
  
    public mutablelivedata<string> getaccountnum(){  
        return accountnum;  
    }  
  
    public mutablelivedata<string> getpassword(){  
        return password;  
    }  
  
}  

xml布局文件

<?xml version="1.0" encoding="utf-8"?>  
<layout xmlns:tools="http://schemas.android.com/tools"  
    xmlns:app="http://schemas.android.com/apk/res-auto">  
  
    <data>  
        <variable  
            name="viewmodel"  
            type="tang.com.databindingcompilerv2.login.loginviewmodel"/>  
  
        <import type="android.view.view"/>  
    </data>  
  
    <android.support.constraint.constraintlayout  
        xmlns:android="http://schemas.android.com/apk/res/android"  
        android:layout_width="match_parent"  
        android:layout_height="match_parent">  
  
        <android.support.design.widget.textinputlayout  
            android:id="@+id/til_account_num"  
            android:layout_width="match_parent"  
            android:layout_height="wrap_content"  
            android:layout_marginend="8dp"  
            android:layout_marginleft="8dp"  
            android:layout_marginright="8dp"  
            android:layout_marginstart="8dp"  
            android:layout_margintop="8dp"  
            app:layout_constraintend_toendof="parent"  
            app:layout_constraintstart_tostartof="parent"  
            app:layout_constrainttop_totopof="parent">  
  
            <android.support.design.widget.textinputedittext  
                android:id="@+id/et_account_num"  
                android:layout_width="match_parent"  
                android:layout_height="wrap_content"  
                android:text="@={viewmodel.account.accountnum}"  
                android:hint="@string/account_prompt"/>  
  
        </android.support.design.widget.textinputlayout>  
  
        <android.support.design.widget.textinputlayout  
            android:id="@+id/til_password"  
            android:layout_width="match_parent"  
            android:layout_height="wrap_content"  
            android:layout_marginend="8dp"  
            android:layout_marginleft="8dp"  
            android:layout_marginright="8dp"  
            android:layout_marginstart="8dp"  
            app:layout_constraintend_toendof="parent"  
            app:layout_constraintstart_tostartof="parent"  
            app:layout_constrainttop_tobottomof="@+id/til_account_num">  
  
            <android.support.design.widget.textinputedittext  
                android:id="@+id/et_password"  
                android:layout_width="match_parent"  
                android:layout_height="wrap_content"  
                android:inputtype="textwebpassword"  
                android:text="@={viewmodel.account.password}"  
                android:hint="@string/password_prompt" />  
  
        </android.support.design.widget.textinputlayout>  
  
        <android.support.v7.widget.appcompatbutton  
            android:layout_width="match_parent"  
            android:layout_height="wrap_content"  
            android:layout_marginbottom="8dp"  
            android:layout_marginend="8dp"  
            android:layout_marginleft="8dp"  
            android:layout_marginright="8dp"  
            android:layout_marginstart="8dp"  
            android:text="@string/login"  
            android:onclick="@{viewmodel.login}"  
            app:layout_constraintbottom_tobottomof="parent"  
            app:layout_constraintend_toendof="parent"  
            app:layout_constraintstart_tostartof="parent" />  
  
        <progressbar  
            android:id="@+id/progressbar"  
            android:layout_width="wrap_content"  
            android:layout_height="wrap_content"  
            android:layout_marginbottom="8dp"  
            android:layout_marginend="8dp"  
            android:layout_marginleft="8dp"  
            android:layout_marginright="8dp"  
            android:layout_marginstart="8dp"  
            android:layout_margintop="8dp"  
            app:layout_constraintbottom_tobottomof="parent"  
            app:layout_constraintend_toendof="parent"  
            app:layout_constraintstart_tostartof="parent"  
            app:layout_constrainttop_totopof="parent"  
            app:isvisible="@{viewmodel.isloading}"  
            />  
  
        <textview  
            android:id="@+id/tv_prompt"  
            android:layout_width="match_parent"  
            android:layout_height="40dp"  
            android:layout_marginend="8dp"  
            android:layout_marginleft="8dp"  
            android:layout_marginright="8dp"  
            android:layout_marginstart="8dp"  
            android:text="@{viewmodel.loginprompt}"  
            app:layout_constraintend_toendof="parent"  
            app:layout_constraintstart_tostartof="parent"  
            app:layout_constrainttop_tobottomof="@+id/til_password" />  
  
    </android.support.constraint.constraintlayout>  
  
</layout>  

viewmodel

public class loginviewmodel extends viewmodel {  
  
    private static final string tag = "loginviewmodel";  
  
    private final mutablelivedata<boolean> isloading = new mutablelivedata<>();  
    private final mutablelivedata<account> account = new mutablelivedata<>();  
    private final mutablelivedata<string> loginprompt = new mutablelivedata<>();  
  
    public loginviewmodel(){  
        account.postvalue(new account("",""));  
        isloading.postvalue(false);  
    }  
  
    public void login(view view){  
        string loginmsg =  "accountnum = " + objects.requirenonnull(account.getvalue()).getaccountnum().getvalue()  
                + "\npassword = " + objects.requirenonnull(account.getvalue()).getpassword().getvalue();  
        log.d(tag,"\n正在登陆中....\n"  
               + loginmsg);  
        loginprompt.postvalue("正在登陆账号:" + objects.requirenonnull(account.getvalue()).getaccountnum().getvalue());  
        isloading.postvalue(true);  
            new handler().postdelayed(() -> {  
                log.d(tag,"登陆成功....\n");  
                isloading.postvalue(false);  
                intent intent = new intent(view.getcontext(), mainactivity.class);  
                intent.putextra("hello", loginmsg);  
                view.getcontext().startactivity(intent);  
                loginprompt.postvalue("");  
            }, 2000);  
  
    }  
  
    public mutablelivedata<boolean> getisloading(){  
        return isloading;  
    }  
  
    public account getaccount(){  
        return account.getvalue();  
    }  
  
    public mutablelivedata<string> getloginprompt() {  
        return loginprompt;  
    }  
}  

activity

public class mainactivity extends appcompatactivity {  
  
    @override  
    protected void oncreate(bundle savedinstancestate) {  
        super.oncreate(savedinstancestate);  
        activitymainbinding binding = databindingutil.setcontentview(this, r.layout.activity_main);  
        binding.sethello(getintent().getstringextra("hello") + "\n hello word !");  
        binding.setlifecycleowner(this);  
  
    }  
}  

到这里,我们就能愉快地data binding compiler v2了。
从测试代码可以看出,代码和我们使用data binding compiler v1的时候差不多,有区别的地方只有两点:

  1. observablefield替换成livedata
  2. binding对象需要调用setlifecycleowner(lifecycleowner lifecycleowner
    )设置lifecycleowner对象。

示例代码

笔者在github上面建立了一个项目,以后所有的文章的测试demo都会上传到这个项目上,有兴趣的读者可以关注下。
这篇文章的示例在项目中的tododatabinding文件下。

项目结构如图所示:
Android开发利器之Data Binding Compiler V2 —— 搭建Android MVVM完全体的基础
其中databindingcompilerv1为data binding compiler v1下的示例代码
其中databindingcompilerv2为data binding compiler v2下的示例代码

data binding compiler v2 示例代码

小结

data binding compiler v2主要是解决了data binding不能感知view层生命周期的问题。
在android开发中我们的控制层(这里指viewmodel)的生命周期和view层组件的生命周期是不能保持一致的,大多数情况下,控制层的生命周期会比view层长。例如,我们发起网络请求的时候,在请求回调之前view有被销毁的可能,如果在view被销毁后控制层再更新view层,这个时候我们就会遇到讨厌的npe异常。lifecycle系列组件的主要功能就是使控制层能够感知view层的生命周期。而data binding compiler v2则是为了使data binding能够使用lifecycle中的livedata从而获得感知生命周期的能力,即达成data binding 的lifecycle-aware。

关于我

github

微信公众号:
如果你觉得这片文章对你有所启发的话,可以关注我的微信公众号哦
Android开发利器之Data Binding Compiler V2 —— 搭建Android MVVM完全体的基础