参考
最全的总结:Android MVP 详解(上)
如何理解Web应用程序的MVC模型?
Android App的设计架构:MVC,MVP,MVVM与架构经验谈
选择恐惧症的福音!教你认清MVC,MVP和MVVM
界面之下:还原真实的MV*模式
浅谈 Android 编程思想和架构
对于屏幕旋转 Activity 重建和内存泄漏等问题,MVP 的解决方案
Android的MVP设计模式
在架构一个应用时,我们要建立模型,说得土一点就是建表,然后设定对这些表的各种可能操作。比如建一个 students 表,允许关联班级、导师,然后定义增删改查的操作,别搞出漏洞或者脏数据……这些事情完成,我们就实现了“领域模型”或者说“业务逻辑”(domain logic/business logic)。
但我们没有办法根据“需求”直接架构这些 Model。需求是多变的,甚至是感性的、碎片化的。而“Model”需要实打实地对应一个面向对象编程意义上的“对象”,追求单一性、里氏可替换、接口分离、开闭原则……一堆原则。
在现实中,我们不可能对“需求”使用“单一性”原则。用户打开一个知乎用户页就想看到他赞过的帖子他发表的回答;打开一篇网易新闻就想看脑残评论;打开一本文艺作品就想聆听习总在文艺座谈会上的讲话……如果把各种需求都塞到模型里,会产生大量冗余和重复的代码。
此外,对象之间天然不擅长走“工作流”,所以涉及“提交了 A 然后进入 B 页面”这种应用层面的流程需求,也很难用 Model 对象表达。
何况,需求之间也是存在“继承”的,比如一个网站的一大波页面都需要展示用户的登录信息和头像。这种数据如果你不想在 V 里面直接读 Model 的话,就只有靠 C 之间的继承来传递了。
如果一个应用同时有网站、移动 APP 和 API 接口就更容易理解了,Model 在不同应用之间应当是可以复用的,因为它走的是“业务逻辑”。
所以,我们不妨把 C 理解为“用户需求的一种抽象”。除了负责倾听用户,还负责给用户所有他需要的数据(这有些接近 ViewModel 的概念了)。这部分东西,其实也被称为“应用逻辑”(application logic)。
一、MVC
- View是把控制权交移给Controller,Controller执行应用程序相关的应用逻辑(对来自View数据进行预处理、决定调用哪个Model的接口等等)。
- Controller操作Model,Model执行业务逻辑对数据进行处理。但不会直接操作View,可以说它是对View无知的。
- View和Model的同步消息是通过观察者模式进行,而同步操作是由View自己请求Model的数据然后对视图进行更新。
需要特别注意的是MVC模式的精髓在于第三点:Model的更新是通过观察者模式告知View的,具体表现形式可以是Pub/Sub或者是触发Events。而网上很多对于MVC的描述都没有强调这一点。通过观察者模式的好处就是:不同的MVC三角关系可能会有共同的Model,一个MVC三角中的Controller操作了Model以后,两个MVC三角的View都会接受到通知,然后更新自己。保持了依赖同一块Model的不同View显示数据的实时性和准确性。我们每天都在用的观察者模式,在几十年前就已经被大神们整合到MVC的架构当中。
优点:
把业务逻辑和展示逻辑分离,模块化程度高。且当应用逻辑需要变更的时候,不需要变更业务逻辑和展示逻辑,只需要Controller换成另外一个Controller就行了(Swappable Controller)。
观察者模式可以做到多视图同时更新。
缺点:
Controller测试困难。因为视图同步操作是由View自己执行,而View只能在有UI的环境下运行。在没有UI环境下对Controller进行单元测试的时候,应用逻辑正确性是无法验证的:Model更新的时候,无法对View的更新操作进行断言。
View无法组件化。View是强依赖特定的Model的,如果需要把这个View抽出来作为一个另外一个应用程序可复用的组件就困难了。因为不同程序的的Domain Model是不一样的
以下例子参考心脏起搏器——MVC的一个简单例子

1.Model(以及模型的观察者要实现的接口)
模型持有所有数据、状态。它不知道视图和控制器的存在,但它实现了观察者模式,将状态改变通知观察者。
model是独立的,它不持有controller和view的引用。
package heart_driver;
import java.util.ArrayList;
//观察者要实现的接口
interface HeartBeatListener {
public void onHeartBeat();
}
class Model {
int frequency;
boolean on;
ArrayList<HeartBeatListener> listeners = new ArrayList<>();
public void addHeartBeatListener(HeartBeatListener listener) {
listeners.add(listener);
}
public void removeHeartBeatListener(HeartBeatListener listener) {
listeners.remove(listener);
}
public void setFrequency(int freq) {
this.frequency = freq;
}
public void on() {
on = true;
new Thread(new Runnable() {
@Override
public void run() {
while(on) {
for(HeartBeatListener listener : listeners)
listener.onHeartBeat();
try {
Thread.sleep(60000/frequency);
} catch (InterruptedException e) {}
}
}
}).start();
}
public void off() {
on = false;
}
}
2.View
用户界面,用来呈现模型,视图从模型中取得它要显示的数据。视图将用户行为都委托给Controler,对工作如何完成毫无知情。
持有controller的引用,在用户操作时,直接交给controller去处理
package heart_driver;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
class View extends JFrame implements HeartBeatListener,ActionListener,ChangeListener {
Controller controller;
JButton onButton,offButton;
JSlider slider;
JProgressBar bar;
public View() {
JPanel buttonPanel = new JPanel();
onButton = new JButton("on");
offButton = new JButton("off");
onButton.addActionListener(this);
offButton.addActionListener(this);
buttonPanel.add(onButton);
buttonPanel.add(offButton);
add(buttonPanel,BorderLayout.NORTH);
bar = new JProgressBar();
bar.setForeground(Color.RED);
add(bar,BorderLayout.CENTER);
slider = new JSlider();
slider.setMaximum(200);
slider.setMinimum(1);
slider.setValue(70);
slider.addChangeListener(this);
JPanel sliderPanel = new JPanel();
sliderPanel.add(new JLabel("频率:"));
sliderPanel.add(slider);
add(sliderPanel,BorderLayout.SOUTH);
pack();
setDefaultCloseOperation(EXIT_ON_CLOSE);
setTitle("心脏起搏器");
off();
setVisible(true);
}
public void setController(Controller controller) {
this.controller = controller;
}
public void on() {
offButton.setEnabled(true);
onButton.setEnabled(false);
}
public void off() {
offButton.setEnabled(false);
onButton.setEnabled(true);
}
@Override
public void onHeartBeat() {
try {
for(int i=0;i<bar.getMaximum();i+=bar.getMaximum()/5) {
bar.setValue(i);
Thread.sleep(20);
}
for(int i=bar.getMaximum();i>=0;i-=bar.getMaximum()/5) {
bar.setValue(i);
Thread.sleep(20);
}
} catch (InterruptedException e) {}
}
@Override
public void stateChanged(ChangeEvent e) {
int freq = ((JSlider)e.getSource()).getValue();
controller.setFrequency(freq);
}
@Override
public void actionPerformed(ActionEvent e) {
//点击按钮后要发生的事情都委托给controller
if(e.getSource()==onButton)
controller.on();
else if(e.getSource()==offButton)
controller.off();
}
}
3.Controller
同时持有了view和model的引用
package heart_driver;
class Controller {
View view;
Model model;
public void setView(View view) {
this.view = view;
}
public void setModel(Model model) {
this.model = model;
}
public void setFrequency(int freq) {
model.setFrequency(freq);
}
public void on() {
view.on();
model.on();
}
public void off() {
view.off();
model.off();
}
}

运行一下:
package heart_driver;
public class HeartDriver {
public static void main(String[] args) {
new HeartDriver();
}
public HeartDriver() {
Controller controller = new Controller();
Model model = new Model();
View view = new View();
view.setController(controller);//view要持有controller,来表达自己的诉求
controller.setModel(model);//controller持有model,传递诉求
controller.setView(view);//controller还持有view,view的一个操作导致界面其它变化
model.addHeartBeatListener(view);//model注册侦听,观察者模式
view.slider.setValue(75);
model.frequency = 75;
}
}
view要持有controller,来表达自己的诉求(view也可能持有model引用,来直接获取数据);controller持有model,传递诉求;controller还持有view,view的一个操作导致界面其它变化。
很容易看出,传统的MVC耦合性非常高。具体来讲,视图层遇到客户请求,发送事件给控制层,控制层为了实现自身的逻辑功能,可能要用到一些视图层的东西,用户请求的表现逻辑和控制层要实现的业务逻辑两者混合起来了,不能合理分工处理自己的事物,造成两部分之间的依赖性过强。
怎么解决这个问题?以下参考从MVC框架看MVC架构的设计
通过引入一个过渡层Mediator,介入视图层(View)和控制层(Controller)之间,这样的设计模式如同将传统的MVC模式扩展一般---PureMVC模式;

具体实现过程:视图层(View)接受用户请求,为了实现表现逻辑会在自身相应的方法中收集自身需要的数据,进行第一次事件广播或者事件派发;此时,Mediator层的监听器会监听到相应的事件,在相应的方法中对其处理,该层根据命令需求,协调相关构件来实现上一层要实现的表现逻辑,进行第二次事件广播或事件派发;紧接着,控制层(Controller)会监听到事件,进行业务逻辑的完成工作;
两次事件广播过程中,Mediator层将视图层和控制层分割了出来,View层和Mediator层和Controller层之间并不存在调用关系,而是二次委派事件的机制来完成的,三层之间独立存在,甚至感应不到彼此的存在;该模式缓解了视图层和控制层紧耦合的问题;
在这方面,事件机制起到了至关重要的作用。事件机制可以让当前对象专注于处理其职责范围内的事务,而不必关心超出部分由谁来处理以及怎样处理,当前对象只需要广播一个事件,就会有对此事件感兴趣的其他对象出来接手下一步的工作,当前对象与接手对象之间不存在直接依赖,甚至感知不到彼此的存在,这是事件机制被普遍认为是一种松耦合机制的重要原因。讲到这里插一句题外话,在领域驱动设计(Domain-Driven Design)里,著名的Domain Event模式也是有赖于事件机制的这一特性被创造出来的,其用意正是为了保证领域模型的纯净,避免领域模型对repository和service的直接依赖。回到PureMVC,我们来看在处理用户请求的过程中,事件机制是如何串联view、mediator和controller的。在PureMVC里,当一个用户请求下达时,图形组件先在自身的事件响应方法中实现与自身相关的展现逻辑,然后收集数据,将数据置入一个新的event中,将其广播出去,这是第一次事件委派。这个event会被一个mediator监听到,如果处理该请求需要其他图形组件的协助,mediator会协调它们处理应由它们承担的展现逻辑,然后mediator再次发送一个event(这次的event在PureMVC里称之为notification),这个event会促使某个command执行,完成业务逻辑的计算,这是第二次事件委派。在两次事件委派中,第一次事件委派让当事图形组件完成“处理其职责范围内的展现逻辑”后,得以轻松“脱身”,免于被“协调其他图件处理剩余展现逻辑”和“选择并委派业务对象处理业务逻辑”所拖累。而“协调其他图形组件处理剩余展现逻辑”显然是mediator的职责,于是第一次广播的事件被委派给了mediator。mediator在完成图形组件的协调工作后,并不会插手“选择并委派业务对象处理业务逻辑”的工作,这不是它的职责,因此,第二次事件委派发生了,一个新的event由mediator广播出去,后被某个command响应到,由command完成了最后的工作——“选择并委派业务对象处理业务逻辑”。
由于缺少合理的组织依据,controller的粒度很难拿捏。controller不同于view与model,view与model都有各自天然的粒度组织依据,view的组织粒度直接承袭用户界面设计,model的组织粒度则是依据某种分析设计思想(如OOA/D)进行领域建模的结果,controller需要同时协调view与model,但是view与model的组织结构和粒度都是不对等的,这就使得controller面临一个“在多大视图范围内沟通与协调多少领域对象”的问题,由于找不出合理的组织依据,设计者在设计controller时往往感到无所适从。相比之下,command则完全没有controller的困惑,因为command有一个天然的组织依据,这就是user action。针对一个user action设计一个command,然后将两者映射在一起,是一件非常自然而简单的事情。不过,需要说明的是这并不意味着所有command的粒度是一样的,因为不同的user action所代表的业务量是不同的,因此也决定了command是有“大”有“小”的。遵循良好的设计原则,对某些较“大”的command进行分解,从中抽离出一些可复用的部分封装成一些较“小”的command是值得推荐的。很多MVC框架就定义了一些相关的接口和抽象类用于支持基于组合模式的命令拼装。
不管是基于controller还是基于command,MVC架构中界定的“协调view与model交互”的控制器职责是不会变的,都需要相应的组件和机制去承载与实现。在基于command的架构里,command承担了过去controller的部分职责,从某种意义上说command是一种细粒度的controller,但是command的特性是偏“被动”的。一方面,它对于view和model的控制力比controller弱化了很多, 比如,一般情况下command是不会直接操纵view的。另一方面,它不知道自己与什么样的user action映射在了一起,也不知道自己会在何种情况下被触发执行。支撑command的运行需要额外的注册、绑定和触发机制,是这些机制加上command一起实现了controller的职责。由于现在多数基于command的MVC框架都实现并封装了这些重要的机制,所以从某种意义上说,是这些框架自身扮演了controller角色。
二、MVP
在android开发中,activity又控制界面又控制逻辑,很容易上千行。把其中的逻辑剥离出去放到一个类中,就叫做Presenter。view和presenter各自抽象出自己的接口,然后双方互相持有对方一个接口类型的引用。view和model就不再发生关联了,全部交给presenter来解决。

和MVC模式一样,用户对View的操作都会从View交移给Presenter。Presenter会执行相应的应用程序逻辑,并且对Model进行相应的操作;而这时候Model执行完业务逻辑以后,也是通过观察者模式把自己变更的消息传递出去,但是是传给Presenter而不是View。Presenter获取到Model变更的消息以后,通过View提供的接口更新界面。
关键点:
View不再负责同步的逻辑,而是由Presenter负责。Presenter中既有应用程序逻辑也有同步逻辑。
View需要提供操作界面的接口给Presenter进行调用。(关键)
对比在MVC中,Controller是不能操作View的,View也没有提供相应的接口;而在MVP当中,Presenter可以操作View,View需要提供一组对界面操作的接口给Presenter进行调用;Model仍然通过事件广播自己的变更,但由Presenter监听而不是View。
优点:
便于测试。Presenter对View是通过接口进行,在对Presenter进行不依赖UI环境的单元测试的时候。可以通过Mock一个View对象,这个对象只需要实现了View的接口即可。然后依赖注入到Presenter中,单元测试的时候就可以完整的测试Presenter应用逻辑的正确性。这里根据上面的例子给出了Presenter的单元测试样例。
View可以进行组件化。在MVP当中,View不依赖Model。这样就可以让View从特定的业务场景中脱离出来,可以说View可以做到对业务完全无知。它只需要提供一系列接口提供给上层操作。这样就可以做到高度可复用的View组件。
缺点:
Presenter中除了应用逻辑以外,还有大量的View->Model,Model->View的手动同步逻辑,造成Presenter比较笨重,维护起来会比较困难。
以下参考Android中的MVC和MVP(分析+实例)
1.首先是MVC的实现方式
public class Essay {
private String title;
private String url;
private String page;
public void setTitle(String title) {
this.title = title;
}
public void setUrl(String url) {
this.url = url;
}
public void setPage(String page) {
this.page = page;
}
public String getTitle() {
return title;
}
public String getUrl() {
return url;
}
public String getPage() {
return page;
}
}
//model:
public class EssayModel {
private Context mContext;
public interface OnEssayListener{
void onSuccess(List<Essay> list);
void onError();
}
public EssayModel(Context context){
mContext = context;
}
private OnEssayListener mListener;
public void getEssay(int num,OnEssayListener listener){
mListener = listener;
ArrayList<Essay> list = new ArrayList<Essay>();
SQLiteDatabase db = new DBHelper(mContext).getWritableDatabase();
String sql = "select title,url,page from essay";
String sql1 = "insert into essay (title,url,page) values(
'This is my essay.','http://www.baidu.com','http://www.baidu.com')";
db.execSQL(sql1);
//获取数据
Cursor cursor = db.rawQuery(sql,null);
cursor.moveToFirst();
Essay tmp = new Essay();
tmp.setTitle(cursor.getString(0));
tmp.setUrl(cursor.getString(1));
tmp.setPage(cursor.getString(2));
list.add(tmp);
listener.onSuccess(list);
cursor.close();
db.close();
}
}
//Activity:
public void updateData(){
EssayModel model = new EssayModel(this);
model.getEssay(1, new EssayModel.OnEssayListener() {
@Override
public void onSuccess(List<Essay> list) {
esssayInfoTv.setText("标题:"+list.get(0).getTitle()+"文章链接:"+list.get(0).getUrl());
}
@Override
public void onError() {
}
});
}
2.MVP
//Activity
EssayPresenter presenter = new EssayPresenter(this,this);
presenter.loadEssay();
public void updataData(Essay essay){
esssayInfoTv.setText("标题:" + essay.getTitle() + "文章链接:" + essay.getUrl());
}
//presenter
public class EssayPresenter {
private EssayView mView;
private EssayModel mEssayModel;
public EssayPresenter(EssayView view,Context context) {
mView = view;
mEssayModel = new EssayModel(context);
}
public void loadEssay() {
Essay essay = new Essay();
essay = mEssayModel.getEssay();
mView.updateData(essay); // 通过调用IUserView的方法来更新显示
}
}
//model:
public Essay getEssay(){
SQLiteDatabase db = new DBHelper(mContext).getWritableDatabase();
String sql = "select title,url,page from essay";
String sql1 = "insert into essay (title,url,page) values(
'This is my essay.','http://www.baidu.com','http://www.baidu.com')";
db.execSQL(sql1);
Cursor cursor = db.rawQuery(sql, null);
cursor.moveToFirst();
Essay tmp = new Essay();
tmp.setTitle(cursor.getString(0));
tmp.setUrl(cursor.getString(1));
tmp.setPage(cursor.getString(2));
cursor.close();
db.close();
return tmp;
}
在Android开发中,Activity并不是一个标准的MVC模式中的Controller,它的首要职责是加载应用的布局和初始化用户界面,并接受并处理来自用户的操作请求,进而作出响应。随着界面及其逻辑的复杂度不断提升,Activity类的职责不断增加,以致变得庞大臃肿。当我们将其中复杂的逻辑处理移至另外的一个类(Presneter)中时,Activity其实就是MVP模式中View,它负责UI元素的初始化,建立UI元素与Presenter的关联(Listener之类),同时自己也会处理一些简单的逻辑(复杂的逻辑交由Presenter处理).
另外,回想一下你在开发Android应用时是如何对代码逻辑进行单元测试的?是否每次都要将应用部署到Android模拟器或真机上,然后通过模拟用户操作进行测试?然而由于Android平台的特性,每次部署都耗费了大量的时间,这直接导致开发效率的降低。而在MVP模式中,处理复杂逻辑的Presenter是通过interface与View(Activity)进行交互的,这说明了什么?说明我们可以通过自定义类实现这个interface来模拟Activity的行为对Presenter进行单元测试,省去了大量的部署及测试的时间。
源码:https://github.com/VectorYi/MVPSample.git


public class UserBean {
private String mFirstName ;
private String mLastName ;
public UserBean (String firstName, String lastName) {
this .mFirstName = firstName;
this .mLastName = lastName;
}
public String getFirstName() {
return mFirstName ;
}
public String getLastName() {
return mLastName ;
}
}
//View可以对ID、FirstName、LastName这三个EditText进行读操作,对FirstName和LastName进行写操作
public interface IUserView {
int getID();
String getFristName();
String getLastName();
void setFirstName (String firstName);
void setLastName (String lastName);
}
//Model也需要对这三个字段进行读写操作,并存储在某个载体内
//(这不是我们所关心的,可以存在内存、文件、数据库或者远程服务器,但对于Presenter及View无影响)
public interface IUserModel {
void setID (int id);
void setFirstName (String firstName);
void setLastName (String lastName);
int getID();
UserBean load (int id);//通过id读取user信息,返回一个UserBean
}
//Presenter能通过接口与View及Model进行交互
public class UserPresenter {
private IUserView mUserView ;
private IUserModel mUserModel ;
public UserPresenter (IUserView view) {
mUserView = view;
mUserModel = new UserModel ();
}
public void saveUser( int id , String firstName , String lastName) {
mUserModel .setID (id );
mUserModel .setFirstName (firstName );
mUserModel .setLastName (lastName );
}
public void loadUser( int id ) {
UserBean user = mUserModel .load (id );
mUserrView .setFirstName (user .getFirstName ());//通过调用IUserView的方法来更新显示
mUserView .setLastName (user .getLastName ());
}
}
//UserModel:
public class UserModel implements IUserModel {
private String mFristName;
private String mLastName;
private int mID;
private SparseArray<UserBean> mUsererArray = new SparseArray<UserBean>();
@Override
public void setID(int id) {
// TODO Auto-generated method stub
mID = id;
}
@Override
public void setFirstName(String firstName) {
// TODO Auto-generated method stub
mFristName = firstName;
}
@Override
public void setLastName(String lastName) {
// TODO Auto-generated method stub
mLastName = lastName;
UserBean UserBean = new UserBean(mFristName, mLastName);
mUsererArray.append(mID, UserBean);
}
@Override
public UserBean load(int id) {
// TODO Auto-generated method stub
mID = id;
UserBean userBean = mUsererArray.get(mID, new UserBean("not found",
"not found"));
return userBean;
}
}
//Activity:
//View只负责处理与用户进行交互,并把数据相关的逻辑操作都扔给了Presenter去做。
//而Presenter调用Model处理完数据之后,再通过IUserView更新View显示的信息。
public class UserActivity extends Activity implements OnClickListener,
IUserView {
private EditText mFirstNameEditText, mLastNameEditText, mIdEditText;
private Button mSaveButton, mLoadButton;
private UserPresenter mUserPresenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findWidgets();
mUserPresenter = new UserPresenter(this);
mSaveButton.setOnClickListener(this);
mLoadButton.setOnClickListener(this);
}
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
switch (v.getId()) {
case R.id.saveButton:
mUserPresenter.saveUser(getID(), getFristName(),
getLastName());
break;
case R.id.loadButton:
mUserPresenter.loadUser(getID());
break;
default:
break;
}
}
@Override
public void setFirstName(String firstName) {
// TODO Auto-generated method stub
mFirstNameEditText.setText(firstName);
}
@Override
public void setLastName(String lastName) {
// TODO Auto-generated method stub
mLastNameEditText.setText(lastName);
}
@Override
public int getID() {
// TODO Auto-generated method stub
return Integer.parseInt(mIdEditText.getText().toString());
}
@Override
public String getFristName() {
// TODO Auto-generated method stub
return mFirstNameEditText.getText().toString();
}
@Override
public String getLastName() {
// TODO Auto-generated method stub
return mLastNameEditText.getText().toString();
}
void findWidgets() {
mFirstNameEditText = (EditText) findViewById(R.id.first_name_edt);
mLastNameEditText = (EditText) findViewById(R.id.last_name_edt);
mIdEditText = (EditText) findViewById(R.id.id_edt);
mSaveButton = (Button) findViewById(R.id.saveButton);
mLoadButton = (Button) findViewById(R.id.loadButton);
}
}
三、MVVM
MVVM的调用关系和MVP一样。但是,在ViewModel当中会有一个叫Binder,或者是Data-binding engine的东西。以前全部由Presenter负责的View和Model之间数据同步操作交由给Binder处理。你只需要在View的模版语法当中,指令式地声明View上的显示的内容是和Model的哪一块数据绑定的。当ViewModel对进行Model更新的时候,Binder会自动把数据更新到View上去,当用户对View进行操作(例如表单输入),Binder也会自动把数据更新到Model上去。这种方式称为:Two-way data-binding,双向数据绑定。可以简单而不恰当地理解为一个模版引擎,但是会根据数据变更实时渲染。
也就是说,MVVM把View和Model的同步逻辑自动化了。以前Presenter负责的View和Model同步不再手动地进行操作,而是交由框架所提供的Binder进行负责。只需要告诉Binder,View显示的数据对应的是Model哪一部分即可。
参考android UI设计MVVM设计模式讨论
网友评论