Android应用架构演变

引言

总结了多年的移动开发经验,特别是在Android端的积累,以前认为从移动端APP谈架构,其实有点举大旗,因为大部份项目都在做业务理,且往往不是很大,并没有多复杂的数据处理或高并发(只针对个人而言);并且长期认为架构这个词,用在Web端或者大型系统里比较好一点,好的架构好,意味系统更稳健、高效率, 更大体量。总之,有种大材小用的感觉,然而,随着Android应用开发规模的扩大,客户端业务逻辑也越来越复杂,已然不是简单的数据展示了,APP也需要进行架构设计,拆分视图和数据,解除模块之间的耦合,提高模块内部的聚合度。

APP 程序的结构

对于开发人员来讲,项目层面决定了如何搭建整个项目及划分模块,常用的Android程序结构:

  • UI层
    数据展示与管理
    用户交互
    绘制
    Adapter
  • 业务逻辑层
    持久化数据(内存中,相当于全局数据)
    数据加式(数据层的数据有时候需要进行加工成UI层需要的数据)
    数据变化的通知机制
  • 数据层
    数据访问(DB,文件,网络等)
    缓存(图片,文件等)
    配置文件(shared perference)

从程序结构看,架构在APP中无处不在,只是我们不太关注,最简单的Demo其实都有涉及架构(通常是MVC)。从Android诞生至今,移动端的架构变更了很多次,从最初的MVC到MVP, 从冷门的Flutter(由RN引入到移动端)到Google的AAC/MVVM;好像架构的思想一直在变,但万变不离其中。下面将依次介绍MVC、MVP、MVVM这几种主流的架构设计,这里不会很深入的分析这些架构的代码上有何区别,只是分析它们的设计思路,在项目中方便的选择适用的架构。

MVC

非常经典的架构,不管哪个平台,都有这样的架构,好用又实惠。Android采用XML文件实现页面布局,通过Java在Activity中开发业务逻辑,这种开发模式实际上已经采用了MVC的思想,分离视图和控制器。MVC模式(Model–view–controller)是软件工程中的一种软件架构模式,把软件系统分为三个基本部分:模型(Model)、视图(View)和控制器(Controller)。

维基百科:
MVC模式最早由Trygve Reenskaug在1978年提出,是施乐帕罗奥多研究中心(XeroxPARC)在20世纪80年代为程序语言Smalltalk发明的一种软件架构。MVC模式的目的是实现一种动态的程序设计,使后续对程序的修改和扩展简化,并且使程序某一部分的重复利用成为可能。除此之外,此模式通过对复杂度的简化,使程序结构更加直观。软件系统通过对自身基本部分分离的同时也赋予了各个基本部分应有的功能。专业人员可以通过自身的专长分组:
- 控制器(Controller)- 负责转发请求,对请求进行处理。
- 视图(View) - 界面设计人员进行图形界面设计。
- 模型(Model) - 程序员编写程序应有的功能(实现算法等等)、数据库专家进行数据管理和数据库设计(可以实现具体的功能)。

在Android编程中,View对应xml布局文件,Model对应实体模型(网络、数据库、I/O),Controller对应Activity业务逻辑,数据处理和UI处理。如下图所示:
MVC

//Model
public interface WeatherModel {
    void getWeather(String cityNumber, OnWeatherListener listener);
}
................

public class WeatherModelImpl implements WeatherModel {
    @Override
    public void getWeather(String cityNumber, final OnWeatherListener listener) {
        /*数据层操作*/
        VolleyRequest.newInstance().newGsonRequest(http://www.weather.com.cn/data/sk/ + cityNumber + .html,
        Weather.class, new Response.Listener<weather>() {
            @Override
            public void onResponse(Weather weather) {
                if (weather != null) {
                    listener.onSuccess(weather);
                } else {
                    listener.onError();
                }
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                listener.onError();
            }
        });
    }
}

//Controllor(View)层
public class MainActivity extends ActionBarActivity implements OnWeatherListener, View.OnClickListener {
    private WeatherModel weatherModel;
    private EditText cityNOInput;
    private TextView city;
    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        weatherModel = new WeatherModelImpl();
        initView();
    }

    //初始化View
    private void initView() {
        cityNOInput = findView(R.id.et_city_no);
        city = findView(R.id.tv_city);
        ...
        findView(R.id.btn_go).setOnClickListener(this);
    }

    //显示结果
    public void displayResult(Weather weather) {
        WeatherInfo weatherInfo = weather.getWeatherinfo();
        city.setText(weatherInfo.getCity());
        ...
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_go:
                weatherModel.getWeather(cityNOInput.getText().toString().trim(), this);
                break;
        }
    }

    @Override
    public void onSuccess(Weather weather) {
        displayResult(weather);
    }

    @Override
    public void onError() {
        Toast.makeText(this, 获取天气信息失败, Toast.LENGTH_SHORT).show();
    }

    private T findView(int id) {
        return (T) findViewById(id);
    }
}

例子分析:

  • Activity里面的控件必须关心业务和数据,才能知道自己怎么展示。
  • 所有的逻辑都在activity里面。

因此,在实际开发过程中,纯粹作为View的各个XML文件功能较弱,Activity基本上都是View和Controller的合体,既要负责视图的显示又要加入控制逻辑,承担的功能很多,导致代码量很大。所有更贴切的目前常规的开发说应该是View-Model模式,大部分都是通过Activity的协调。

MVP

MVP是从MVC过渡而来,MVP架构由三部分组成:View负责显示,Presenter负责逻辑处理,Model提供数据。Android开发从MVC过渡到MVP,最主要的变化就是将Activity中负责业务逻辑的代码移到Presenter中,Activity只充当MVP中的View,负责界面初始化以及建立界面控件与Presenter的关联。

例如,MVC中,在业务逻辑稍微复杂一点的页面,Activity的代码超过一千是很容易的,但Activity并不是一个标准的MVC模式中的Controller,它的首要职责是加载应用的布局和初始化用户界面,并接受并处理来自用户的操作请求,进而作出响应。随着界面及其逻辑的复杂度不断提升,Activity类的职责不断增加,以致变得庞大臃肿,那自然会想到进行拆分。这样拆分之后,Presenter承担了大量的逻辑操作,避免了Activity的臃肿。整个架构如下图所示:
MVP

//Model层
/**
 * 定义业务接口
 */
public interface IUserBiz {
    public void login(String username, String password, OnLoginListener loginListener);
}

/**
 * 结果回调接口
 */
public interface OnLoginListener {
    void loginSuccess(User user);

    void loginFailed();
}

/**
 * 具体Model的实现
 */
public class UserBiz implements IUserBiz {
    @Override
    public void login(final String username, final String password, final OnLoginListener loginListener) {
        //模拟子线程耗时操作
        new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //模拟登录成功
                if ("zhy".equals(username) && "123".equals(password)) {
                    User user = new User();
                    user.setUsername(username);
                    user.setPassword(password);
                    loginListener.loginSuccess(user);
                } else {
                    loginListener.loginFailed();
                }
            }
        }.start();
    }
}


//View
public interface IUserLoginView {
    String getUserName();

    String getPassword();

    void clearUserName();

    void clearPassword();

    void showLoading();

    void hideLoading();

    void toMainActivity(User user);

    void showFailedError();
}


//然后Activity实现这个这个接口:
public class UserLoginActivity extends ActionBarActivity implements IUserLoginView {
    private EditText mEtUsername, mEtPassword;
    private Button mBtnLogin, mBtnClear;
    private ProgressBar mPbLoading;
    private UserLoginPresenter mUserLoginPresenter = new UserLoginPresenter(this);

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_user_login);
        initViews();
    }

    private void initViews() {
        mEtUsername = (EditText) findViewById(R.id.id_et_username);
        mEtPassword = (EditText) findViewById(R.id.id_et_password);
        mBtnClear = (Button) findViewById(R.id.id_btn_clear);
        mBtnLogin = (Button) findViewById(R.id.id_btn_login);
        mPbLoading = (ProgressBar) findViewById(R.id.id_pb_loading);
        mBtnLogin.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mUserLoginPresenter.login();
            }
        });
        mBtnClear.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mUserLoginPresenter.clear();
            }
        });
    }

    @Override
    public String getUserName() {
        return mEtUsername.getText().toString();
    }

    @Override
    public String getPassword() {
        return mEtPassword.getText().toString();
    }

    @Override
    public void clearUserName() {
        mEtUsername.setText("");
    }

    @Override
    public void clearPassword() {
        mEtPassword.setText("");
    }

    @Override
    public void showLoading() {
        mPbLoading.setVisibility(View.VISIBLE);
    }

    @Override
    public void hideLoading() {
        mPbLoading.setVisibility(View.GONE);
    }

    @Override
    public void toMainActivity(User user) {
        Toast.makeText(this, user.getUsername() +
                " login success , to MainActivity", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void showFailedError() {
        Toast.makeText(this,
                "login failed", Toast.LENGTH_SHORT).show();
    }
}


//Presenter
public class UserLoginPresenter {
    private IUserBiz userBiz;
    private IUserLoginView userLoginView;
    private Handler mHandler = new Handler();

    //Presenter必须要能拿到View和Model的实现类
    public UserLoginPresenter(IUserLoginView userLoginView) {
        this.userLoginView = userLoginView;
        this.userBiz = new UserBiz();
    }

    public void login() {
        userLoginView.showLoading();
        userBiz.login(userLoginView.getUserName(), userLoginView.getPassword(), new OnLoginListener() {
            @Override
            public void loginSuccess(final User user) {
                //需要在UI线程执行
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        userLoginView.toMainActivity(user);
                        userLoginView.hideLoading();
                    }
                });
            }

            @Override
            public void loginFailed() {
                //需要在UI线程执行
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        userLoginView.showFailedError();
                        userLoginView.hideLoading();
                    }
                });
            }
        });
    }

    public void clear() {
        userLoginView.clearUserName();
        userLoginView.clearPassword();
    }
}

说明:

  • Model通过回调的方式将数据传到Presenter中;
  • View(Activity)负责响应用户操作,通过Presenter暴露的方法请求数据;
  • Presenter在获取数据后,通过View(Activity)暴露的方法实现界面控制,通过Model来获取数据,Model包含网络、数据库以及I/O等。

采用MVP明显的优点是避免了传统开发模式中View和Model耦合的情况,提高了代码可扩展性、组件复用能力、团队协作的效率以及单元测试的便利性。但也有一些缺点,比如:

  • Model到Presenter的数据传递过程需要通过回调;
  • View(Activity)需要持有Presenter的引用,同时,Presenter也需要持有View(Activity)的引用,增加了控制的复杂度;
  • MVC中Activity的代码很臃肿,转移到MVP的Presenter中,同样造成了Presenter在业务逻辑复杂时的代码臃肿。

所以,MVC到MVP简单说,就是增加了一个接口降低一层耦合。

MVVM

MVVM 架构模式是微软在 2005 年诞生的,MVVM是Model-View-ViewModel的简称,它由三个部分组成,也就是 Model、View 和 ViewModel,其中视图模型(ViewModel)其实就是 PM 模式中的展示模型,在 MVVM 中叫做视图模型。从实际效果来看,ViewModel是View的数据模型和Presenter的结合,具体结构如下图所示:

说明:

  • Model(模型层)通过网络和本地数据库获取视图层所需数据;
  • View(视图层)采用XML文件进行界面的描述;
  • ViewModel(视图-模型层)负责View和Model之间的通信,以此分离视图和数据。

View和Model之间通过Android Data Binding技术,实现视图和数据的双向绑定;ViewModel持有Model的引用,通过Model的方法请求数据;获取数据后,通过Callback(回调)的方式回到ViewModel中,由于ViewModel与View的双向绑定,使得界面得以实时更新。同时,界面输入的数据变化时,由于双向绑定技术,ViewModel中的数据得以实时更新,提高了数据采集的效率。

MVVM架构将Presenter改名为ViewModel,基本上与MVP模式完全一致,唯一的区别是,它采用双向绑定(data-binding)View的变动,自动反映在 ViewModel,反之亦然,这就导致了我们如果要完整的采用 MVVM 必须熟练的掌握 DataBinding 等基础组建,这就给我们MVVM引入项目带了困难。

总结

其实,MVC、MVP及MVVM没有绝对好坏,在软件编程过程中,也没必要非此即彼,脱离实际项目比较这些模式优劣毫无意义,各种模式都有优点和缺点,没有好坏之分。越高级的架构实现起来越复杂,需要更多的学习成本更多的人力,所以说技术选型关键是在你自己项目的特点,团队的水平,资源的配备,开发时间的限制,这些才是重点!但是不少团队本末倒置,把mvvm往自己的项目硬套。最重要的是让软件高内聚、低耦合、可维护、可扩展。

可以理解为移动端的架构思维“MVX”,即是说按这个规则的分工,我们不用费尽心思考虑狭义架构的分层问题了,就沿用Model-View-X来就可以(当然还可以自己加一些辅助的模块层)。


参考资料:

  • 《Android App的架构设计:从VM、MVC、MVP到MVVM》
  • 《如何写出无法维护的代码》
  • 《谈谈我理解的Android应用架构》