MiQing Blog

无惧艰难,让生活更幸福更充实,努力起飞!

0%

【Android-开发框架搭建】-学会使用MVVM架构的搭建

前言

开发过程中常见的框架模式有MVC、MVP、MVVM,本篇文章讲解MVVM框架模式的原理及在Android开发中如何实现及使用该框架模式。这里一定要明白MVVM不是某些技术实现而是架构模式或者说是设计思路,我们根据MVVM的设计原理进行架构开发,最终形成的是一系列代码组合而成的MVVM模式的框架,MVVM是Android开发最好的架构选择之一,也是Google官方非常支持和鼓励开发者使用这一架构模式,在这个框架上开发出来的程序稳定性更高,可扩展性更强。

MVVM原理

MVVM模式不是四层,任然是3层,是Model-View-ViewModel的简写,VM是ViewModel的缩写,VM可以理解为View的数据模型和Presenter的合体,ViewModel和View之间的交互通过data binding完成。下面我们挨个来来说Model、View、ViewModel。

Model

Model层就是职责数据的存储、读取网络数据、操作数据库数据以及I/O,它就是用来提供数据,不关心数据去做什么。

View

View层做的仅仅和UI相关的工作,在Android中我们只在XML、Activity、Fragment写View层的代码,View层不做和业务相关的事,也就是我们的Activity 不写和业务逻辑相关代码,一般Activity不写更新UI的代码,如果非得要写,那更新的UI必须和业务逻辑和数据是没有关系的,只是单纯UI逻辑来更新UI,比如:滑动时头部颜色渐变、editttext根据输入内容显示隐藏等,简单的说:View层不做任何业务逻辑、不涉及操作数据、不处理数据、UI和数据严格的分开。

ViewModel

ViewModel 只做和业务逻辑和业务数据相关的事,不做任何和UI、控件相关的事,ViewModel 层不会持有任何控件的引用,更不会在ViewModel中通过UI控件的引用去做更新UI的事情。ViewModel就是专注于业务的逻辑处理,操作的也都是对数据进行操作,这些个数据源绑定在相应的控件上会自动去更改UI,开发者不需要关心更新UI的事情。

此处贴出MVVM的模型图
示例1

ViewModel是如何让View和Model实现数据绑定,达到自动更新以及数据更新双向的绑定关系呢?
Google在2018年推出了Android Jetpack组件,Jetpack是一个Android软件组件的集合,其中AAC(Android Architecture Components)框架是android官方提供的一系列组件,用来非常方便的实现MVVM架构。
本文主要讲解AAC中Lifecycle、LiveData、ViewModel以及DataBinding的结合使用搭建能够基础开发的MVVM架构项目。

添加依赖

1
2
//Google AAC
implementation 'android.arch.lifecycle:extensions:1.1.1'

MVVM使用

DataBinding

Data Binding(数据绑定)发布于2015年的Google I/O大会,旨在减少Android开发中的大量模板代码(比如findViewById()),增加代码及逻辑清晰度,提高开发效率和维护效率。

什么是Data Binding?
Data Binding,即数据绑定,是Android团队实现MVVM架构的一种方法,使得数据(对象)可以直接绑定到布局的xml中,数据的变化直接反映到View上。
同时,Data Binding也支持双向绑定。

有什么好处?
省去大量模板代码,比如findViewById(),setOnClickListener(), setText(),等等。
使得View与逻辑彻底解耦(MVVM)成为可能,不像MVC那样逻辑与View操作混在一起难以维护,也不像MVP那样定义大量接口,费时费力。
由于数据(对象)与View进行双向绑定,所以开发时只需要关注数据(对象)即可,无需关心View的各种繁杂操作(如setVisibility(),setText()等)
功能强大,xml中即可完成简单的逻辑(xml中支持表达式语言,逻辑/数学运算等)

如何使用?

1.在模块build.gradle中配置如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
android {

dataBinding {
enabled = true
}
}
Gradle 插件 4.0.0及以上:
android {
buildFeatures{
dataBinding = true
// for view binding :
// viewBinding = true
}
}

2.修改页面布局结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
正常页面布局:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/userName"
app:layout_constraintTop_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</androidx.constraintlayout.widget.ConstraintLayout>

支持DataBinding页面布局:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:binding="http://schemas.android.com/apk/res-auto"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>

</data>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/userName"
app:layout_constraintTop_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

3.使用DataBinding绑定页面及数据
每个数据绑定布局文件都会生成一个绑定类ViewDataBinding的实例名是根据布局文件名来生成,将之改为首字母大写的驼峰命名法来命名,并省略布局文件名包含的下划线。控件的获取方式类似,但首字母小写
也可以通过如下方式自定义ViewDataBinding的实例名

1
2
3
<data class="CustomBinding">

</data>

绑定页面方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
在Activity中使用
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
mBinding.userName.setText("懂了不");
}
在Fragment中使用
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
FragmentMainBinding binding = DataBindingUtil.inflate(inflater, R.layout.fragment_main, container, false);
mBinding.userName.setText("懂了不");
return binding.getRoot();
}

绑定数据方式:
在这个标签中,我们通常用来做下面的事情:
定义所绑定的数据的名称(变量名)及对应类型
引入页面所需的类
示例如下:
其中””标签表示引入一个类,比如下方例中引入了User类,当然也可以引入其它类,比如常量类或者工具类。
下面””标签定义了本页面所需要的各种数据名称或类型,其类型可以是java中的基础类型,或者自定义的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class User {
private String userName;
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:binding="http://schemas.android.com/apk/res-auto"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="com.mi.qing.common.net.mvvm.User"/>
<!--非import方式使用-->
<!-- <variable-->
<!-- name="user"-->
<!-- type="com.mi.qing.common.net.mvvm.User" />-->

<!--import方式使用-->
<variable
name="user"
type="User" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/userName"
app:layout_constraintTop_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

绑定User数据到布局页面中,并且在TextView中显示使用,@{}格式绑定数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
mBinding.userName.setText("懂了不");
User user = new User();
user.setUserName("明白了不");
mBinding.setUser(user);
}


页面布局TextView中使用User中的userName数据
<TextView
android:id="@+id/userName"
app:layout_constraintTop_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:text="@{user.userName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

Data Binding 还支持以下操作符:

  • 运算符 + - / * %
  • 字符串连接 +
  • 逻辑与或 && ||
  • 二进制 & | ^
  • 一元 + - ! ~
  • 移位 >> >>> <<
  • 比较 == > < >= <= (Note that < needs to be escaped as <)
  • instanceof
  • Grouping ()
  • Literals - character, String, numeric, null
  • Cast
  • 方法调用
  • 域访问
  • 数组访问
  • 三元操作符

4.使用ObservableField刷新UI
?它其实是一个可观察的
域,通过泛型来使用,可以使用的方法也就三个:

方法 作用
ObservableField(T value) 构造函数,设置可观察的域
T get() 获取可观察的域的内容,可以使用UI控件监测它的值
set(T value) 设置可观察的域,设置成功之后,会通知UI控件进行更新

不过,除了使用ObservableField 之外, Data Binding 为我们提供了基本类型的ObservableXXX (如
ObservableInt )和存放容器的ObservableXXX (如ObservableList )等,同样,如果你想让你自定义的类变成
可观察状态,需要实现Observable 接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class UserModel {
public ObservableField<User> userObservableField = new ObservableField<>();

}

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
mBinding.userName.setText("懂了不");
UserModel userModel = new UserModel();
mBinding.setUserModel(userModel);
User user = new User();
user.setUserName("明白了不");
userModel.userObservableField.setUser(user);
}


页面布局data标签中绑定数据
<data>
<variable
name="userModel"
type="包名.UserModel" />
</data>

页面布局TextView中使用User中的userName数据
<TextView
android:id="@+id/userName"
app:layout_constraintTop_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:text="@{userModel.userObservableField.userName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

5.双向数据绑定
双向绑定的意思即为当数据改变时同时使视图刷新,而视图改变时也可以同时改变数据,比如当EditText输入的内容在TextView上实时显示,同时TextView绑定的数据也会更新
仅仅在将@{userModel.userObservableField.userName} 替换为@={userModel.userObservableField.userName} ,多了一个= 号而已,需要注意的是,属性必须是可观察
的,可以使用上面提到的ObservableField ,也可以自定义实现BaseObservable 接口,双向绑定的时候需要注意
无限循环,更多关于双向绑定还请查看官方文档。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
页面布局TextView中使用User中的userName数据
<TextView
android:id="@+id/userName"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@id/userName_edit"
android:text="@{userModel.userObservableField.userName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

<EditText
android:id="@+id/userName_edit"
app:layout_constraintLeft_toRightOf="@id/userName"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
android:text="@={userModel.userObservableField.userName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

6.自定义属性BindingAdapter
dataBinding 提供了 BindingAdapter 这个注解用于支持自定义属性,或者是修改原有属性。注解值可以是已有的 xml 属性,例如 android:src、android:text等,也可以自定义属性然后在 xml 中使用
例如,对于一个 ImageView ,我们希望在某个变量值发生变化时,可以动态改变显示的图片,此时就可以通过 BindingAdapter 来实现
需要先定义一个静态方法,为之添加 BindingAdapter 注解,注解值是为 ImageView 控件自定义的属性名,而该静态方法的两个参数可以这样来理解:
当 ImageView 控件的 url 属性值发生变化时,dataBinding 就会将 ImageView 实例以及新的 url 值传递给 loadImage() 方法,从而可以在此动态改变 ImageView 的相关属性

1
2
3
4
5
6
public class ImageUtils {
@BindingAdapter({"url"})
public static void loadImage(ImageView view, String url) {
Log.e(TAG, "loadImage url : " + url);
}
}

在 xml 文件中关联变量值,当中,bind 这个名称可以自定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">

<data>
<variable
name="imageUtils"
type="包名.ImageUtils" />
</data>

<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
>

<ImageView
android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
bind:url="@{imageUtils.url}" />

</android.support.constraint.ConstraintLayout>
</layout>

DataBinding的其它使用方式参考 文章地址

ViewModel & LiveData

1.LiveData
LiveData 作用跟RxJava类似,是观察数据的类,相比RxJava,它能够在Activity、Fragment和Service之中正确的处理生命周期。这意味着它尊重其他应用程序组件的生命周期,这种感知确保LiveData只更新处于生命周期状态内的应用程序组件。
LiveData是由observer类表示的观察者视为处于活动状态,如果其生命周期处于START或RESUMED状态。LiveData会将观察者视为活动状态,并通知其数据的变化。LiveData未注册的观察对象以及非活动观察者是不会收到有关更新的通知。
那么LiveData 有什么优点呢?

  • 数据变更的时候更新UI
  • 没有内存泄漏
  • 不会因为停止Activity崩溃
  • 无需手动处理生命周期
  • 共享资源

使用方式:

方法名 作用
observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) 最常用的方法,需要提供Observer 处理数据变更后的处理。LifecycleOwner 则是我们能够正确处理声明周期的关键!
getValue():T 获取数据
postValue(T value) 在主线程中更新数据

2.ViewModel
ViewModel 具有生命周期意识的处理跟UI相关的数据,并且,当设备的一些配置信息改变(例如屏幕旋转)它的数据不会消失。
通常情况下,如果我们不做特殊处理,当屏幕旋转的时候,数据会消失,那ViewModel 管理的数据为什么不会消失呢,是因为ViewModel 的生命周期:
示例2
ViewModel 的另一个特点就是同一个Activity 的Fragment 之间可以使用ViewModel实现共享数据。

3.定义ViewModel和创建LiveData

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class UserModel extends AndroidViewModel{

// 创建LiveData
public MutableLiveData<User> mUser = new MutableLiveData<>();

public AccountModel(@NonNull Application application) {
super(application);
}

public void getUser(){
User user = new User();
user.setUserName("明白了不");
mUser.postValue(user);
}

// 当MyActivity被销毁时,Framework会调用ViewModel的onCleared()
@Override
protected void onCleared() {
Log.e("AccountModel", "==========onCleared()==========");
super.onCleared();
}
}

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
UserModel userModel = ViewModelProviders.of(this).get(UserModel.class);
mBinding.setUserModel(userModel);
userModel.getUser();
}


<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:bind="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">

<data>
<variable
name="userModel"
type="包名.UserModel" />
</data>

<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
>

<TextView
android:id="@+id/userName"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@id/userName_edit"
android:text="@{userModel.mUser.userName}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</android.support.constraint.ConstraintLayout>
</layout>

LiveData的其它使用方式参考 文章地址
ViewModel的其它使用方式参考 文章地址

总结

此处只是简单介绍Android原生支持的MVVM架构模式的组件结合方式,目前github还有比较成熟的框架可以去借鉴一下。
MVVMHabit GITHUB传送门
此处再贴出作者参考搭建的MVVM框架源码
源码地址 MiQingWang/CommonNetFrame

-------- 本文章已被掏空 学无止境下一章 --------