JectPack组件开发2-----MVVM架构核心(DataBinding + ViewModel)

在之前的章节中,已经介绍过JectPack的两个组件,都与生命周期的感知有关----Lifecycle + LiveData,那么本节将会介绍跟数据绑定有关的两个组件DataBinding + ViewModel,从而引出MVVM架构的设计实现。

1、MVVM介绍

跟MVP不同的是,在MVP的P层中,主要做逻辑处理,像请求数据等;在MVVM中,是通过ViewModel代替了P层,ViewModel实现了View层和Model层的双向绑定,数据的绑定就是通过DataBinding这个组件实现的。

JectPack组件开发2-----MVVM架构核心(DataBinding + ViewModel)_第1张图片
首先,要想使用MVVM,那么就要你的项目支持DataBinding,在build.gradle中,添加一行代码

 dataBinding{
        enabled true
    }

2、DataBinding + ViewModel

一般来说,在使用MVVM架构的时候,更多的是在做XML布局和DataBean,一个布局对应一个页面,该页面的数据来源,通过ViewModel来提供,使用DataBinding与数据源绑定,因此在XML布局文件中,要声明该页面的数据来源。

(1)XML布局:在XML布局中,需要设置layoutdata标签,layout标签用来包裹整体的布局,data则是声明在该布局中使用到的DataBean类,如果有多个DataBean,那么就设置多个variable

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <variable
        	//这个databean的别名
            name="user"
            //databean的全类名
            type="com.example.mvvm.User" />
    </data>

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@{user.username}"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@{user.password}"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</LinearLayout>

</layout>

(2)DataBean:在MVVM中,就是ViewModel层,跟之前的DataBean不同的是,这个DataBean类所要做的工作可是很多的,一方面需要监听数据是否变化,然后将数据更新在XML布局文件;另一方面,也会监听XML布局文件中数据的变化,去更新数据库数据,这就是双向绑定。

/**
 * ViewModel
 */
public class User extends BaseObservable {
    private String username;
    private String password;

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }
    @Bindable
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
        notifyPropertyChanged(BR.username);
    }
    @Bindable
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
        notifyPropertyChanged(BR.password);
    }
}

DataBean要继承BaseObservable 就是一个被观察者,XML布局会查询数据,然后在get方法需要加 @Bindable注解,获取当前的返回数据值,与XML布局中的android:text="@{user.username}"绑定;当数据源的数据发生变化时,在set方法中,加入notifyPropertyChanged(BR.user);属性值变化,BR.user就是在布局文件中设置的DataBean的别名,这样在数据发生变化时,就会同步更新到手机界面。

在完成View层和ViewModel层的基本工作完成之后,要在Activity中完成DataBinding

  super.onCreate(savedInstanceState);
//        setContentView(R.layout.activity_main);
        ActivityMainBinding mainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);

        //从Model层来
        user = new User("kobe","123456");
        mainBinding.setUser(user);

通过DataBindingUtil加载布局,会根据当前Activity的名字生成一个ActivityMainBinding 类,一般来说,我们在Model层会进行网路请求,或者数据库请求,会将数据回调到View层界面,在View层做数据调用更新UI,然后在XML布局中已经设置了DataBean的种类,所以通过ActivityMainBinding 来去设置数据,就可以将数据更新在界面。

当网路数据源发生更新之后,也会同步更新在与之绑定的布局文件上。

如果是加载一张图片到ImageView,可以通过自定义属性,来将图片加载到界面。

 //自定义属性
    @BindingAdapter("bind:header")
    public static void getImage(ImageView view,String url){
        Glide.with(view.getContext()).load(url).into(view);
    }
public String getHeader() {
        return header;
    }

    public void setHeader(String header) {
        this.header = header;
    }

    public User(String username, String password,String header) {
        this.username = username;
        this.password = password;
        this.header = header;
    }

然后在布局文件中:

 <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        bind:header="@{user.header}"></ImageView>

其中内部的原理就是:在加载布局时,因为layoutdata标签,会分割为两部分,其中一部分,将这些@属性转换为tag

<TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:tag="binding_1"        
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:tag="binding_2"        
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:tag="binding_3"     ></ImageView>

一些Tag的集合,然后在编译时生成的代表该Activity的DataBinding的实现类,从这些tags集合中,取出实例化,来取代findViewById

 this.mboundView0 = (android.widget.LinearLayout) bindings[0];
        this.mboundView0.setTag(null);
        this.mboundView1 = (android.widget.TextView) bindings[1];
        this.mboundView1.setTag(null);
        this.mboundView2 = (android.widget.TextView) bindings[2];
        this.mboundView2.setTag(null);
        this.mboundView3 = (android.widget.ImageView) bindings[3];
        this.mboundView3.setTag(null);
        setRootTag(root);

然后,通过获取ViewModel中的数据,将数据填充到视图中。

@Override
    protected void executeBindings() {
        long dirtyFlags = 0;
        synchronized(this) {
            dirtyFlags = mDirtyFlags;
            mDirtyFlags = 0;
        }
        com.example.mvvm.User user = mUser;
        java.lang.String userHeader = null;
        java.lang.String userUsername = null;
        java.lang.String userPassword = null;

        if ((dirtyFlags & 0xfL) != 0) {


            if ((dirtyFlags & 0x9L) != 0) {

                    if (user != null) {
                        // read user.header
                        userHeader = user.getHeader();
                    }
            }
            if ((dirtyFlags & 0xbL) != 0) {

                    if (user != null) {
                        // read user.username
                        userUsername = user.getUsername();
                    }
            }
            if ((dirtyFlags & 0xdL) != 0) {

                    if (user != null) {
                        // read user.password
                        userPassword = user.getPassword();
                    }
            }
        }
        // batch finished
        if ((dirtyFlags & 0xbL) != 0) {
            // api target 1

            androidx.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView1, userUsername);
        }
        if ((dirtyFlags & 0xdL) != 0) {
            // api target 1

            androidx.databinding.adapters.TextViewBindingAdapter.setText(this.mboundView2, userPassword);
        }
        if ((dirtyFlags & 0x9L) != 0) {
            // api target 1

            com.example.mvvm.User.getImage(this.mboundView3, userHeader);
        }
    }

除了上述的方式之外,在项目开发中,使用最多的,还是列表,无论是ListView,还是RecyclerView…涉及到适配器的有很多,我这里简单地用ListView介绍一下,其他的在后续项目中,如果用到,就再解释。

使用ListView时,最重要的还是数据(List数据),因此在数据源中,需要自定义属性,来得到所要展示的List数据。

public class ListBean {
    private List<User> list;

    public ListBean(List<User> list) {
        this.list = list;
    }

    public List<User> getList() {
        return list;
    }

    public void setList(List<User> list) {
        this.list = list;
    }

    @BindingAdapter("app:list")
    public static void getAdapter(ListView view,List<User> list){
        view.setAdapter(new ListViewAdapter(view.getContext(),list));
    }
}

两个参数ListView和其对应的参数,使用BindingAdapter注解,然后在布局文件中声明数据源。

 <variable
            name="rank"
            type="com.example.mvvm.ListBean" />
<ListView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:list="@{rank.list}"></ListView>

在Activity中,进行数据绑定。

 User user1 = new User();
        user1.setUsername("lili");

        User user2 = new User();
        user2.setUsername("lili");

        List<User> datas = new ArrayList<>();
        datas.add(user1);
        datas.add(user2);
        ListBean bean = new ListBean(datas);
        mainBinding.setRank(bean);

在ListView的适配器中,同样也使用DataBinding,这个使用就比较简单了。

<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable
            name="user"
            type="com.example.mvvm.User" />
    </data>
<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/tv_rank"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@{user.username}"
        android:textSize="20dp"></TextView>

</LinearLayout>

</layout>

在适配器中,代码简直节省了不止一点!!

@Override
    public View getView(int position, View convertView, ViewGroup parent) {
//        ViewHolder viewHolder = null;
//        if(convertView == null){
//            convertView = View.inflate(context,R.layout.item_rank,null);
//            viewHolder = new ViewHolder();
//            viewHolder.tv_rank = convertView.findViewById(R.id.tv_rank);
//            convertView.setTag(viewHolder);
//        }else{
//            viewHolder = (ViewHolder) convertView.getTag();
//        }
//        viewHolder.tv_rank.setText(datas.get(position).getUsername());
        ItemRankBinding binding = null;
        if(convertView == null) {
             binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.item_rank, parent, false);
        }else{
             binding = DataBindingUtil.getBinding(convertView);
        }
        binding.setUser(datas.get(position));
        
        return binding.getRoot().getRootView();
    }

我只贴出关键代码,之前使用ListView的时候,和使用的方式对比一下,代码很简洁,因为不用findViewById,所以ViewHolder也不必写了。

Button点击事件

在之前使用Button单击事件时,常规的用法是:findViewById + 设置单击事件监听,然后获取数据。

        btn_search.setOnClickListener(new View.OnClickListener() {
            @Override
           public void onClick(View v) {
               String city = et_city.getText().toString();
               //获取数据
                ModelImpl.getInstance().getWeatherByCity(city,
                        "7****************5");
            }
        });

如果使用DataBinding来处理,就简洁了许多:设置Activity为变量之一,得到Activity中该Button的单击事件函数,就可以响应点击事件,不需要去findViewById

<variable
            name="activity"
            type="com.example.weather.view.activity.MainActivity" />
<Button
        android:id="@+id/btn_search"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="查询"
        android:textSize="20dp"
        android:onClick="@{activity.onSearchClick}"
        android:layout_below="@id/et_city"></Button>
//别忘记绑定数据
binding.setActivity(this);

public void onSearchClick(View view){
        String city = et_city.getText().toString();
        //获取数据
        ModelImpl.getInstance().getWeatherByCity(city,
                "7b153f32bcfb18ff064c519e5e7a6b35");
    }

各类操作符的使用

算术运算符 + - / * %
字符串连接运算符 +
逻辑运算符 && ||
二元运算符 & | ^
一元运算符 + - ! ~
移位运算符 >> >>> <<
比较运算符 == > < >= <=(请注意,< 需要转义为 &lt;instanceof
分组运算符 ()
字面量运算符 - 字符、字符串、数字、null

Null 合并运算符

如果左边运算数不是 null,则 Null 合并运算符 (??) 选择左边运算数,如果左边运算数为 ,则选择右边运算数。

android:text="@{user.displayName ?? user.lastName}"

这在功能上等效于:

android:text="@{user.displayName != null ? user.displayName :
user.lastName}"

你可能感兴趣的:(JectPack组件开发2-----MVVM架构核心(DataBinding + ViewModel))