对Android App程序进行架构设计的原因,归根到底是为了提高生产力。通过架构设计使程序模块化,做到模块内部的高聚合和模块之间的低耦合。这样做的好处是使得程序在开发的过程中,开发人员只需要专注于一点,提高程序开发的效率,并且更容易进行后续的测试以及定位问题。但设计不能违背目的,对于不同量级的工程,具体架构的实现方式必然是不同的,切忌犯为了设计而设计,为了架构而架构的毛病。举例而言,一个Android App若只有3个Java文件,那只需要做好模块和层次之间的划分就可以,引入框架或者架构反而提高了工作量,降低了生产力。但若当前开发的App最终代码量过大,本地需要进行复杂操作,同时也需要考虑到与其余的Android开发者以及后台开发人员之间的同步配合,那就需要在架构上进行一些思考。
1. MVC
1.1. MVC简介
MVC全称是
Model View Controller
,是**模型(model)-视图(view)-控制器(controller)**的缩写,其中M层用来处理数据、业务逻辑等;V层用来处理界面的显示结果;C层则起到桥梁的作用,来控制V层和M层通信以达到分离视图显示和业务逻辑层的目的,其结构如下图所示。MVC用一种业务逻辑、数据、界面显示分离的方法来组织代码,在改进和个性化定制界面及用户交互时,不需要重新编写业务逻辑。
1.2. Android中的MVC
- 视图层View:一般采用XML文件进行界面的描述,这些XML可以理解为Android App的View。使用时可以非常方便的引入。同时便于后期界面的修改。逻辑中与界面对应的id不变化则代码不用修改,大大增强了代码的可维护性;
- 控制层Controller:Android App的控制层通常由Activity负责。由于Android中的Activity的响应时间短,若在Activity中处理耗时操作,程序很容易被回收掉。因此,不在Activity中编写逻辑代码,而是将需要的逻辑代码通过Activity交付给Model业务逻辑层进行处理;
- 模型层Model:针对业务模型所建立的数据结构以及相关的类等都可以理解为Android App的Model,Model与View无关,而与业务相关。对数据库的操作、对网络的操作以及业务计算等操作都应该在Model中进行处理。
1.3. 优缺点
-
优点:理解比较容易,技术含量不高,开发成本较低也易于维护与修改。
-
缺点:xml作为view层,控制能力太弱。对于动态改变一个页面的背景或者动态隐藏/显示一个按钮等操作,都无法在xml中进行,只能把相应的处理代码写在Activity中,造成Activity既是Controller层,又是View层这样的一个窘境;
在Android开发中,Activity并不是一个标准的MVC模式中的Controller,它的首要职责是加载应用的布局和初始化用户界面,并接受和处理来自用户的操作请求,进而作出响应。随着界面及其逻辑的复杂度不断提升,Activity类的职责不断增加,以致变得庞大臃肿;
Android中的MVC模式,View层和Model层是相互可知的,即两层之间存在耦合,对于开发、测试、维护等都需要耗费大量的精力。
1.4. MVC实例
模拟登陆界面,输入正确密码并点击登录按钮时,Toast“登录成功”,若密码或账号错误,则Toast“登录失败”,若不输入,则Toast“用户名和密码不能为空”:
-
activity_main.xml
:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<EditText
android:id="@+id/et_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:hint="用户名"/>
<EditText
android:id="@+id/et_pwd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:hint="密码"/>
<Button
android:id="@+id/btn_login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:text="登录"/>
</LinearLayout>
-
MainActivity.java
:
package com.example.mvctest;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
// 输入用户姓名
private EditText mEditUserName;
// 输入用户密码
private EditText mEditPwd;
// 登陆
private Button mBtLogin;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mEditUserName = findViewById(R.id.et_username);
mEditPwd = findViewById(R.id.et_pwd);
btn_login = findViewById(R.id.btn_login);
btn_login.setOnClickListener(view -> {
String userName = mEditUserName.getText().toString();
String pwd = mEditPwd.getText().toString();
// 用户名和密码有输入则登陆,否则弹出提示
if (!TextUtils.isEmpty(userName) && !TextUtils.isEmpty(pwd)) {
login(userName, pwd);
} else {
Toast.makeText(MainActivity.this, "用户名和密码不能为空", Toast.LENGTH_SHORT).show();
}
});
}
private void login(final String userName, final String pwd) {
new Thread(new Runnable() {
@Override
public void run() {
// 沉睡两秒模拟登录
SystemClock.sleep(2 * 1000);
// runOnUiThread切回主线程更新UI
if (userName.equals("kobe") && pwd.equals("24")) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Log.d(TAG, "登录成功");
Toast.makeText(MainActivity.this, "登录成功", Toast.LENGTH_SHORT).show();
}
});
} else {
runOnUiThread(new Runnable() {
@Override
public void run() {
Log.d(TAG, "登录失败");
Toast.makeText(MainActivity.this, "登录失败", Toast.LENGTH_SHORT).show();
}
});
}
}
}).start();
}
}
分析
MainActivity.java
得到以下的结果:
MainActivity
既是Controller层,又是View层,没有实现Controller层和View层的分离;业务逻辑(本例中的login()与UI逻辑)都写在同一个Activity里,代码冗杂,对于简单的项目也许没什么影响和明显的弊端,甚至显得方便,但是一旦项目大了,这样写会使可读性非常低,不利于项目后期的诸多工作。
注
:
SystemClock.sleep(long ms)
方法。该方法在返回之前等待给定的毫秒数(
uptimeMillis
)。类似于
java.lang.Thread#sleep(long)
,但不抛出
InterruptedException
异常。线程的
interrupt()
事件被延迟到下一个可中断操作。直到超过指定的毫秒数才返回。该方法的源码如下:
/**
* Waits a given number of milliseconds (of uptimeMillis) before returning.
* Similar to {@link java.lang.Thread#sleep(long)}, but does not throw
* {@link InterruptedException}; {@link Thread#interrupt()} events are
* deferred until the next interruptible operation. Does not return until
* at least the specified number of milliseconds has elapsed.
*
*/
public static void sleep(long ms)
{
long start = uptimeMillis();
long duration = ms;
boolean interrupted = false;
do {
try {
Thread.sleep(duration);
}
catch (InterruptedException e) {
interrupted = true;
}
duration = start + ms - uptimeMillis();
} while (duration > 0);
if (interrupted) {
// Important: we don't want to quietly eat an interrupt() event,
// so we make sure to re-interrupt the thread so that the next
// call to Thread.sleep() or Object.wait() will be interrupted.
Thread.currentThread().interrupt();
}
}
对比
Thread.sleep()
。
Thread.sleep()
会抛出异常,而
SystemClock.sleep()
不会抛出异常。通过查看源码发现,
SystemClock.sleep()
其实调用的就是
Thread.sleep()
方法。两者本质的区别就是:
SystemClock.sleep()
不能被中断,无论如何都会让当前线程休眠指定的时间,而
Thread.sleep()
可以被中断,有可能在指定的休眠时间前被中断。
2. MVP
2.1. MVP简介
MVP全称为
Model-View-Presenter
,是从MVC演变而来,如下图所示。MVP框架由3部分组成:View负责显示(View interface),Presenter负责逻辑处理,Model提供数据:
- View:负责绘制UI元素、与用户进行交互(在Android中体现为Activity等);
- Model:负责存储、检索、操纵数据(有时也实现一个Model interface用来降低耦合);
- Presenter:作为View与Model交互的中间纽带,处理与用户交互的业务逻辑。
- View interface:需要View实现的接口,View通过View interface与Presenter进行交互,降低耦合,方便进行单元测试。
View层一般包括Activity,Fragment,Adapter等直接和UI相关的类,View层的Activity在启动之后实例化相应的Presenter层,App的控制权后移,由View层转移到Presenter层,两者之间的通信通过Broadcast、Handler或者接口完成,只传递事件和结果。例如:View层通知Presenter层:用户点击了一个Button,Presenter层自己决定应该用什么行为进行响应,该找哪个Model去做这件事,最后Presenter层将完成的结果更新到View层。
2.2. 优缺点
-
优点:
- 在MVP中,Activity的代码不臃肿;
- Model与视图完全分离,所有的交互都发生在Presenter内部,可以只修改视图而不影响Model;
- 可以将一个Presenter用于多个视图,而不需要改变Presenter的逻辑;
- 将逻辑放在Presenter中,可以脱离用户接口来测试这些逻辑(单元测试)。
- 缺点:
-
Presenter层与View层是通过接口进行交互的,接口粒度不好控制。粒度太小,就会存在大量接口的情况,使代码太过碎版化;粒度太大,解耦效果不好。同时对于UI的输入和数据的变化,需要手动调用View层或者Presenter层相关的接口,相对来说缺乏自动性、监听性;
- MVP是以UI为驱动的模型,更新UI都需要保证能获取到控件的引用,同时要考虑当前是否是UI线程,也要考虑 Activity的生命周期(是否已经销毁等);此外,数据都是被动地通过UI控件做展示,但是由于数据的时变性,更希望数据能转被动为主动,能更具活性,由数据来驱动UI。
- View层与Presenter层还是有一定的耦合度。一旦View层某个UI元素更改,那么对应的接口就必须得改,数据如何映射到UI上、事件监听接口这些都需要转变,牵一发而动全身。此外,复杂的业务同时也可能会导致Presenter层太大,代码臃肿的问题依然不能解决。
2.3. MVP实例
模拟登陆界面,输入正确密码并点击登录按钮时,Toast“登录成功”,若密码或账号错误,则Toast“登录失败”,若不输入,则Toast“用户名和密码不能为空”:
- activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<EditText
android:id="@+id/et_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:hint="用户名"/>
<EditText
android:id="@+id/et_pwd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:hint="密码"/>
<Button
android:id="@+id/btn_login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="20dp"
android:text="登录"/>
</LinearLayout>
- UI逻辑抽象成BaseView接口:
package com.example.mvptest;
/**
* 登陆成功、登录失败、弹出Toast等UI逻辑抽象成BaseView接口
*/
public interface BaseView {
void showToast(String msg) ;
void loginSuccess(String msg) ;
void loginFailed(String msg) ;
}
- 把业务逻辑抽象成Presenter接口:
package com.example.presenter;
import com.example.model.User;
import com.example.mvptest.BaseView;
/**
* 绑定视图、解绑视图以及登陆等业务逻辑抽象成Presenter接口
*/
public interface BasePresenter {
// 绑定view
void attachView(BaseView v);
void detachView();
void login(User user);
}
- Model:
package com.example.model;
/**
* User Model
*/
public class User {
private String name;
private String pwd;
public User(String name, String pwd) {
this.name = name;
this.pwd = pwd;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
}java
}
- 实例化各组件,实例化Model类对象,实现UI逻辑接口的MainActivity.java:
package com.example.mvptest;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import com.example.model.User;
import com.example.presenter.MainPresenter;
/**
* 实例化各组件,实例化model类对象,实现UI逻辑接口
*/
public class MainActivity extends AppCompatActivity implements BaseView {
private static final String TAG = "MainActivity";
private EditText et_username;
private EditText et_pwd;
private Button btn_login;
// 声明业务逻辑类
private MainPresenter mainPresenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
et_username = findViewById(R.id.et_username);
et_pwd = findViewById(R.id.et_pwd);
btn_login = findViewById(R.id.btn_login);
mainPresenter = new MainPresenter();
// 绑定view,把this付给业务逻辑类中的全局变量,从而实现具体的业务逻辑
mainPresenter.attachView(this);
btn_login.setOnClickListener(view -> {
// 创建含有name和password的User对象
User user = new User(et_username.getText().toString(), et_pwd.getText().toString());
// 调用具体的业务逻辑方法
mainPresenter.login(user);
});
}
@Override
public void showToast(String msg) {
Log.d(TAG, "showToast is executed");
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
}
@Override
public void loginSuccess(String msg) {
Log.d(TAG, "loginSuccess is executed");
showToast(msg);
}
@Override
public void loginFailed(String msg) {
Log.d(TAG, "loginFailed is executed");
showToast(msg);
}
}
- 实现业务逻辑接口的 MainPresenter.java:
package com.example.presenter;
import android.text.TextUtils;
import android.util.Log;
import com.example.model.User;
import com.example.mvptest.BaseView;
/**
* 书写业务逻辑
*/
public class MainPresenter implements BasePresenter {
private static final String TAG = "MainPresenter";
private BaseView baseView;
@Override
public void attachView(BaseView v) {
// 绑定view
this.baseView = v;
}
@Override
public void detachView() {
// 解绑View
baseView = null;
}
@Override
public void login(User user) {
if (!TextUtils.isEmpty(user.getName()) && !TextUtils.isEmpty(user.getPwd())) {
if (user.getName().equals("kobe") && user.getPwd().equals("24")) {
baseView.loginSuccess("登陆成功");
Log.d(TAG, "登陆成功");
} else {
baseView.loginFailed("登录失败");
Log.d(TAG, "登录失败");
}
} else {
baseView.showToast("用户名或密码不能为空");
Log.d(TAG, "用户名或密码不能为空");
}
}
}
对应抽象业务逻辑接口BasePresenter的业务逻辑实现类MainPresenter,用于实现对应的接口;将业务逻辑抽象出来,实现在业务逻辑实现类中。当MainActivity.java中要使用对应的业务逻辑的时候,只需要简简单单实例化一个对应的业务逻辑实现类的对象,再用它调用自定义方法(如
attachView()
),实现对应的业务逻辑;
也就是说,现在Activity要使用业务逻辑的话就不用再在写具体的业务逻辑了,而只需要:实例化业务逻辑实现类的对象,绑定
this
和业务逻辑实现类的对象;使用对象并以相关数据为参数调用相关的业务逻辑方法实现即可;
3. MVVM
3.1. MVVM简介
MVVM全称是
Model-View-ViewModel
,是一种基于前端开发的架构模式,其核心是提供对View和ViewModel 的双向数据绑定,这使得ViewModel的状态改变可以自动传递给View,即所谓的数据双向绑定。其中:
- View是视图层,也就是用户界面,方便展现ViewModel或者Model层的数据;
- Model是指数据模型层,泛指各种业务逻辑处理和数据操控,主要围绕数据库系统展开;
- ViewModel是视图数据层,前端开发者从后端获取得到Model数据进行转换,做二次封装,以生成符合View层使用预期的视图数据模型。视图状态和行为都封装在ViewModel里,使得ViewModel可以完整地去描述View层。
MVVM源自于经典的MVC模式,其核心是ViewModel层,负责转换Model层中的数据对象来让数据变得更容易管理和使用,如下图所示。在MVVM架构中,是不允许View层和Model层直接通信的,只能通过ViewModel来通信。ViewModel和View之间的交互通过DataBinding完成,而DataBinding可以实现双向交互,这就使得视图和控制层之间的耦合程度进一步降低,关注点分离更为彻底,同时减轻了Activity的压力。因此,ViewModel就是连接View层和Model层的中间件。
- ViewModel能够观察到Model层的变化,并对View层对应的内容进行更新,即向上与View层进行双向数据绑定;
- ViewModel能够监听到View层的变化,并能够通知Model层发生变化,即向下与Model层通过接口请求进行数据交互。
注
:与Android DataBinding相关的内容可见
Android databinding详解(layout解析)
以及
Android databinding详解(activity解析)
3.2. 优缺点
-
优点:
- 低耦合。View可以独立于Model的变化和修改,一个ViewModel可以绑定到不同的View上,当View变化时Model可以不变,当Model变化时,View也可以不变;
- 可复用。可以把一些视图逻辑放到一个ViewModel里面,让很多View重用这段视图逻辑;
- 独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计。
- 缺点:数据双向绑定不利于代码重用;
- 数据绑定使得 Bug 很难被调试。数据绑定使得一个位置的 Bug 被快速传递到别的位置,要定位原始出问题的地方就变得不那么容易了。
3.3. MVVM实例
-
activity_main.xml
:
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 定义该布局需要绑定的数据名称和类型-->
<data>
<variable
name="viewModel"
type="com.example.mvvm.LoginViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<EditText
android:id="@+id/et_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入账号"
android:text="@={viewModel.mUserInfo.name}" />
<EditText
android:id="@+id/et_pwd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入密码"
android:text="@={viewModel.mUserInfo.pwd}" />
<Button
android:id="@+id/bt_login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="@{viewModel.mLoginListener}"
android:text="登录" />
</LinearLayout>
</layout>
-
Model:
UserInfo.java
作为
javaBean
比较简单,实际项目中Model包括真正获取数据的实现。
package com.example.mvvm;
import androidx.databinding.ObservableField;
public class UserInfo {
public ObservableField<String> name = new ObservableField<>();
public ObservableField<String> pwd = new ObservableField<>();
@Override
public String toString() {
return "UserInfo{" +
"name=" + name.get() +
", pwd=" + pwd.get() +
'}';
}
public ObservableField<String> getName() {
return name;
}
public void setName(ObservableField<String> name) {
this.name = name;
}
public ObservableField<String> getPwd() {
return pwd;
}
public void setPwd(ObservableField<String> pwd) {
this.pwd = pwd;
}
}
-
ViewModel:
LoginViewModel.java
包含View和Model之间的逻辑代码。
package com.example.mvvm;
import android.content.Context;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import com.example.mvvm.databinding.ActivityMainBinding;
public class LoginViewModel {
private static final String TAG = "LoginViewModel";
Context mContext;
public UserInfo mUserInfo;
public LoginViewModel(ActivityMainBinding binding, Context context) {
// 通过DataBinding工具将ViewModel和View绑定
binding.setViewModel(this);
this.mContext = context;
mUserInfo = new UserInfo();
}
public View.OnClickListener mLoginListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
if ("kobe".equals(mUserInfo.name.get()) && "824".equals(mUserInfo.pwd.get())) {
Toast.makeText(mContext, "Login success, userInfo: "
+ mUserInfo.toString(), Toast.LENGTH_SHORT).show();
Log.d(TAG, "Login success, userInfo: " + mUserInfo.toString());
} else {
Toast.makeText(mContext, "Login failed", Toast.LENGTH_SHORT).show();
Log.d(TAG, "Login failed");
}
}
};
}
-
View:
最后在
MainActivity.java
中完成绑定。
package com.example.mvvm;
import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil;
import android.os.Bundle;
import com.example.mvvm.databinding.ActivityMainBinding;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
LoginViewModel loginViewModel = new LoginViewModel(binding, this);
}
}
-
利用
DataBinding
的双向绑定,结合MVVM架构实现了简单的登录界面,运行效果图如下:
4. 总结
MVC→MVP→MVVM是一步步演化发展的。MVP是隔离了MVC中的Model与View的直接联系后,靠Presenter进行中转,因此,在使用MVP时,Presenter直接调用View的接口来实现对视图的操作。而MVVM则是MVP的进一步发展与规范,Model与View已经分离了,但代码还不够优雅简洁,所以MVVM就弥补了这些缺陷。在MVVM中出现的Data Binding概念,即View 接口的showData这些实现方法可以不写了,通过Binding来实现。
-
同 :共同点在于Model和View
- Model:数据对象,同时提供本应用外部对应用程序数据的操作的接口,也可能在数据变化时发出变更通知;
- Model不依赖于View的实现,只要外部程序调用Model的接口就能够实现对数据的增删改查;
- View:UI层,提供对最终用户的交互操作功能,包括UI展现代码及一些相关的界面逻辑代码。
-
异:不同点在于如何粘合View和Model,实现用户的交互操作以及变更通知
- Controller:接收View的操作事件,根据事件不同,或者调用Model的接口进行数据操作,或者进行View的跳转,从而也意味着一个Controller可以对应多个View。Controller对View的实现不太关心,只会被动地接收,Model的数据变更不通过Controller直接通知View,通常View采用观察者模式监听Model的变化;
- Presenter:与Controller一样,接收View的命令,对Model进行操作。与Controller不同的是,Presenter会反作用于View,Model的变更通知首先被Presenter获得,然后Presenter再去更新View。一个Presenter只对应于一个View;
- ViewModel:注意这里的“Model”指的是View的Model,跟MVVM中的一个Model不是一回事。所谓View的Model就是包含View的一些数据属性和操作的类,这种模式的关键技术就是数据绑定(data binding),View的变化会直接影响ViewModel,ViewModel的变化或者内容也会直接体现在View上。