Android App设计架构

  • Post author:
  • Post category:其他


对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上。



版权声明:本文为m0_50417702原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。