对Android App程序进行架构设计的原因,归根到底是为了提高生产力。通过架构设计使程序模块化,做到模块内部的高聚合和模块之间的低耦合。这样做的好处是使得程序在开发的过程中,开发人员只需要专注于一点,提高程序开发的效率,并且更容易进行后续的测试以及定位问题。但设计不能违背目的,对于不同量级的工程,具体架构的实现方式必然是不同的,切忌犯为了设计而设计,为了架构而架构的毛病。举例而言,一个Android App若只有3个Java文件,那只需要做好模块和层次之间的划分就可以,引入框架或者架构反而提高了工作量,降低了生产力。但若当前开发的App最终代码量过大,本地需要进行复杂操作,同时也需要考虑到与其余的Android开发者以及后台开发人员之间的同步配合,那就需要在架构上进行一些思考。
MVC全称是Model View Controller,是**模型(model)-视图(view)-控制器(controller)**的缩写,其中M层用来处理数据、业务逻辑等;V层用来处理界面的显示结果;C层则起到桥梁的作用,来控制V层和M层通信以达到分离视图显示和业务逻辑层的目的,其结构如下图所示。MVC用一种业务逻辑、数据、界面显示分离的方法来组织代码,在改进和个性化定制界面及用户交互时,不需要重新编写业务逻辑。
优点:理解比较容易,技术含量不高,开发成本较低也易于维护与修改。
缺点:xml作为view层,控制能力太弱。对于动态改变一个页面的背景或者动态隐藏/显示一个按钮等操作,都无法在xml中进行,只能把相应的处理代码写在Activity中,造成Activity既是Controller层,又是View层这样的一个窘境;
在Android开发中,Activity并不是一个标准的MVC模式中的Controller,它的首要职责是加载应用的布局和初始化用户界面,并接受和处理来自用户的操作请求,进而作出响应。随着界面及其逻辑的复杂度不断提升,Activity类的职责不断增加,以致变得庞大臃肿;
Android中的MVC模式,View层和Model层是相互可知的,即两层之间存在耦合,对于开发、测试、维护等都需要耗费大量的精力。
模拟登陆界面,输入正确密码并点击登录按钮时,Toast“登录成功”,若密码或账号错误,则Toast“登录失败”,若不输入,则Toast“用户名和密码不能为空”:
activity_main.xml
:
<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()
可以被中断,有可能在指定的休眠时间前被中断。
MVP全称为Model-View-Presenter ,是从MVC演变而来,如下图所示。MVP框架由3部分组成:View负责显示(View interface),Presenter负责逻辑处理,Model提供数据:
View层一般包括Activity,Fragment,Adapter等直接和UI相关的类,View层的Activity在启动之后实例化相应的Presenter层,App的控制权后移,由View层转移到Presenter层,两者之间的通信通过Broadcast、Handler或者接口完成,只传递事件和结果。例如:View层通知Presenter层:用户点击了一个Button,Presenter层自己决定应该用什么行为进行响应,该找哪个Model去做这件事,最后Presenter层将完成的结果更新到View层。
模拟登陆界面,输入正确密码并点击登录按钮时,Toast“登录成功”,若密码或账号错误,则Toast“登录失败”,若不输入,则Toast“用户名和密码不能为空”:
<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>
package com.example.mvptest;
/**
* 登陆成功、登录失败、弹出Toast等UI逻辑抽象成BaseView接口
*/
public interface BaseView {
void showToast(String msg) ;
void loginSuccess(String msg) ;
void loginFailed(String msg) ;
}
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);
}
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
}
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);
}
}
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
和业务逻辑实现类的对象;使用对象并以相关数据为参数调用相关的业务逻辑方法实现即可;
MVVM全称是Model-View-ViewModel,是一种基于前端开发的架构模式,其核心是提供对View和ViewModel 的双向数据绑定,这使得ViewModel的状态改变可以自动传递给View,即所谓的数据双向绑定。其中:
MVVM源自于经典的MVC模式,其核心是ViewModel层,负责转换Model层中的数据对象来让数据变得更容易管理和使用,如下图所示。在MVVM架构中,是不允许View层和Model层直接通信的,只能通过ViewModel来通信。ViewModel和View之间的交互通过DataBinding完成,而DataBinding可以实现双向交互,这就使得视图和控制层之间的耦合程度进一步降低,关注点分离更为彻底,同时减轻了Activity的压力。因此,ViewModel就是连接View层和Model层的中间件。
注:与Android DataBinding相关的内容可见Android databinding详解(layout解析)以及Android databinding详解(activity解析)
activity_main.xml
:
<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架构实现了简单的登录界面,运行效果图如下:MVC→MVP→MVVM是一步步演化发展的。MVP是隔离了MVC中的Model与View的直接联系后,靠Presenter进行中转,因此,在使用MVP时,Presenter直接调用View的接口来实现对视图的操作。而MVVM则是MVP的进一步发展与规范,Model与View已经分离了,但代码还不够优雅简洁,所以MVVM就弥补了这些缺陷。在MVVM中出现的Data Binding概念,即View 接口的showData这些实现方法可以不写了,通过Binding来实现。