株洲新程IT教育 李赞红
Android Architecture Blueprints是Google官方版本的MVP实现,我们通过https://github.com/googlesamples/android-architecture/tree/todo-mvp可以阅读具体源码。AAB用于TODO计划任务的管理,主要有TODO列表、添加新的TODO、查看TODO详情与TODO统计等功能,总体来说,该案例具有较强的参考价值,特别是在数据存储方面提供了三种方式:远程数据存储、本地数据存储和缓存存储,并使用了标准的MVP设计模式,如图1所示。
图1:AndroidArchitecture Blueprints 的MVP实现
本文将结合AndroidArchitecture Blueprints实现一个业务更简单的案例,一方面帮助大家理解MVP的设计思路与实现过程,另一方面把常见的查询、添加和删除操作作为参考实现,方便大家理解。
MVP从MVC架构模式演化而来, MVC分别代表模型、视图和控制器,在Android中,定义Class类作为模型,Layout XML表示视图,而Activity用作控制器,这样一来,在Activity中充斥了大量代码,无论是从扩展性和重用性都无法达到理想的效果。个人以为,MVC分层在Android App开发中显得极为牵强。而MVP解决了这个问题。
MVP即为Model、View和Presenter。Model表示模型,实现数据存储与业务逻辑;View表示视图,提供用户交互的接口;Presenter表示主导器,相当于MVC中的Controller但比Controller更灵活。MVP的关系如图2所示。
图2:MVP关系
从上图可以看出:
A) View将功能委托给Presenter完成,Presenter调用Model完成业务功能与数据存储,并再次通过View更新UI;
B) View和Model没有直接关联,无法相互调用;
C) Presenter和View可以相互调用;
D) Presenter调用Model完成业务功能。
根据Google官方说明,Fragment作为View层的实现组件,包含Presenter的引用;Model就是一个普通的类,使用了单例模式获取对象;Presenter虽然也是一个普通的类,但拥有View和Model的引用,是一个八面玲珑的家伙。
使用Fragment而不是Activity作为View,主要有两个原因:
A)通过Activity和Fragment分离更适合对于MVP架构的实现,Activity将作为全局的控制者将Presenter于View联系在一起;
B)采用Fragment更有利于平板电脑的布局或者是多视图屏幕。
我们为View和Presenter定义了最基本的实现,View的基本实现为BaseView接口,Presenter的基本实现为BasePresenter接口。
public interface BaseView
void setPresenter(T presenter);
}
public interface BasePresenter {
void start();
}
BasePresenter接口中定义了start()方法,主要用于完成一些初始化的工作,BaseView接口定义的setPresenter()方法则用于建立与Presenter的关系。
MVP的优点归纳如下:
A) 各个层次之间的职责更加单一清晰;
B) 很大程度上降低了代码的耦合度;
C) 复用性大大提高;
D) 面向接口编程,定义与实现分离,方便测试与维护;
E) 代码更简洁。
有优点自然有缺点:
A) 类变得更多了;
B) 组件与组件之间的关系很复杂。
案例AndroidArchitecture Blueprints严格遵循面向接口编程,同时,数据存储模块提供了面向接口编程的最佳实践,当我们怀疑面向接口编程的必要性时,Google通过现实案例马上打消了我们的这种想法。面向接口编程是一门优雅而美好的代码艺术,功能之间通过轻量级的接口联系在一起,摆脱了低级趣味的依赖关系。通过这种极度抽象的上层建筑来维护组件之间的关联,大大增强了代码的艺术性、维护性与重用性。
所以,View要定义接口,Presenter要定义接口,Model也要定义接口。
每一个界面都涉及到五个组件:View、Presenter、Contract、Model和Activity。其中,View定义了与UI相关的操作,比如提示信息的显示与隐藏、状态的改变、数据的显示、新Activity的打开等等,Fragment将实现View接口;Presenter是功能实现的粘合剂,View的功能委托给Presenter,Presenter调用Model完成业务功能,最后Presenter再次调用View来更新UI;Contract将View与Presenter两个接口进行集中管理;Model实现业务功能与数据存储;Activity是界面显示的起点,负责创建其他各个组件,并依次维护各组件之间的关系。
在Android ArchitectureBlueprints案例中,是以界面功能为单位对package进行定义的:
图3:package划分
如图3所示,添加和修改TODO定义在addedittask包中,TODO详情定义在taskdetail包中,TODO列表显示定义在tasks包中,TODO数据统计定义在statistics包中。而data包定义了Model有关的类,util包主要是一些工具类。
现有一个接受客户意见和反馈的建议收集模块,主要有我的建议列表、提建议和删除我的建议等功能,我们以提建议为例通过面向接口编程的思想来定义主要的功能接口。首先,我们定义一个Model层的接口AdviceDataSource,该接口定义如下:
public interfaceAdviceDataSource {
public void addAdvice(@NonNull Stringcontent, @NonNull int uid, @NonNull AddedAdviceCallBack callBack);
public interface AddedAdviceCallBack{
void onAdviceAdded();
}
}
方法addAdvice()用于添加一条新建议,参数content表示建议的内容,uid表示用户id。考虑到所有请求都是基于网络的异步请求,方法最后的参数指定了一个回调接口,用于响应请求结束后的后续动作(比如添加成功后要关闭当前Activity)。
接下来,定义AddAdviceContract接口,该接口集中管理View和Presenter两个接口:
public interfaceAddAdviceContract {
public interface Presenter extendsBasePresenter{
public void addAdvice(String content);
}
public interface View extendsBaseView
public void showAdvicesList();
}
}
代码中AddAdviceContract .Presenter接口继承自BasePresenter接口,定义了addAdvice()方法,该方法将在Fragment中调用,接收来自Fragment中的建议内容,并传递给AdviceDataSource.addAdvice()方法进行处理(保存到远程服务器)。AddAdviceContract .View接口是BaseView的派生接口,定义了showAdvicesList()方法,该方法用于添加完建议后,关闭当前Activity以便重新加载并显示建议列表。
现在我们来理顺提建议这个功能的调用流程:在实现了View接口的Fragment中,调用AddAdviceContract .Presenter的addAdvice()方法提交新的建议,该方法又调用AdviceDataSource. addAdvice()方法将建议内容保存到远程服务器,确定保存成功后,在AddAdviceContract .Presenter的addAdvice()方法中调用AddAdviceContract.View的showAdvicesList()方法关闭当前窗口以显示新的建议列表。
总体来说,在View中所有的功能都要委托给Presenter,Presenter调用Model完成任务后再次通过View更新结果,如图4所示。
图4:各组件之间的调用顺序
本案例中使用了株洲新程IT教育提供的数据接口,在这里,我们统一使用id为5的用户作为测试,数据接口的url如下(不保证以后能用):
A)获取用户的建议列表
http://tr.api.gson.cn/system/feedback/{用户id}
请求类型:get
B)提交建议
http://tr.api.gson.cn/system/feedback/{用户id}
请求类型:post
参数:content 建议内容
C)删除指定建议
http://tr.api.gson.cn/system/feedback/delete/{用户id}/{建议id}
请求类型:get
为了简化访问HTTP请求,使用了xUtils3.x第三方库,该工具主要集成了view、bitmap、db、http和task五大模块,简单易用,值得推荐。数据接口返回的是json格式的数据,阿里的fastjson具有强大的JSON数据解析功能,牛逼得不要不要的。
这个功能与MVP无关,但为了保持案例的完整性,顺便提一下。定义MvpApplication类如下:
public classMvpApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
x.Ext.init(this);
x.Ext.setDebug(true);
}
}
在AndroidManifest.xml配置文件中,配置自定义的Application。
package="com.example.mvp"> android:name=".MvpApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> 注意,访问网络数据必须授予android.permission.INTERNET权限。 我们定义了一个名为Advice的JavaBean,用于保存建议的基本属性,并生成了一系列的getter方法和setter方法。 @HttpResponse(parser= AdviceDataSource.AdviceJSONResponseParser.class) public classAdvice { private int id; //建议id private int uid; //用户id private String content; //建议内容 private String reply; //回复内容 private Date createTime;//创建时间 private Date replydate;//回复时间 private int replyUid;//回复用户id public Advice() { } public Advice(int uid, String content, DatecreateTime) { this.uid = uid; this.content = content; this.createTime = createTime; } public Advice(int id, int uid, Stringcontent, String reply, Date createTime, Date replydate, int replyUid) { this.id = id; this.uid = uid; this.content = content; this.reply = reply; this.createTime = createTime; this.replydate = replydate; this.replyUid = replyUid; } public int getId() { return id; } public void setId(int id) { this.id = id; } public int getUid() { return uid; } public void setUid(int uid) { this.uid = uid; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public String getReply() { return reply; } public void setReply(String reply) { this.reply = reply; } public Date getCreateTime() { return createTime; } public void setCreateTime(Date createTime){ this.createTime = createTime; } public Date getReplydate() { return replydate; } public void setReplydate(Date replydate) { this.replydate = replydate; } public int getReplyUid() { return replyUid; } public void setReplyUid(int replyUid) { this.replyUid = replyUid; } } 在Class中定义了@HttpResponse(parser= AdviceDataSource.AdviceJSONResponse Parser.class)元注释,这是xUtils提供的功能,指定了AdviceJSONResponseParser解析器,用于将响应结果中的json数据解析为Advice对象。AdviceJSONResponseParser定义在AdviceDataSource接口中,AdviceDataSource接口还定义了建议列表、提建议和删除建议的功能方法,定义如下: public interfaceAdviceDataSource { public class AdviceJSONResponseParserimplements ResponseParser{ @Override public void checkResponse(UriRequesturiRequest) throws Throwable { } @Override public Object parse(Type type,Class> aClass, String s) throws Throwable { JSONObject json =JSON.parseObject(s); if(aClass == List.class) { if(json.getBoolean("success")) { JSONArray data =json.getJSONArray("data"); List for (int i = 0; i JSONObject current =data.getJSONObject(i); Advice advice = newAdvice(current.getIntValue("id"), current.getIntValue("uid"), current.getString("content"), current.getString("reply"), current.getDate("create_time"), current.getDate("reply_date"), current.getIntValue("reply_uid")); list.add(advice); } return list; } }else{ return s; } return null; } } public void addAdvice(@NonNull Stringcontent, @NonNull int uid, @NonNull AddedAdviceCallBack callBack); public void getMyAdvices(@NonNull int uid,@NonNull LoadAdviceCallBack callBack); public void deleteAdvice(@NonNull int id,@NonNull int uid, @NonNullDeletedAdviceCallBack callBack); public interface LoadAdviceCallBack{ void onAdviceLoaded(List } public interface DeletedAdviceCallBack{ void onAdviceDeleted(); } public interface AddedAdviceCallBack{ void onAdviceAdded(); } } AdviceRepository类实现了AdviceDataSource接口,使用单例模式获取AdviceRepository对象。通过xUtils提供的http模块非常简单方便地实现了数据读写的功能。 public classAdviceRepository implements AdviceDataSource{ private static AdviceRepositorysAdviceRepository; private AdviceRepository() { } public static AdviceRepository getInstance(){ if(sAdviceRepository == null){ sAdviceRepository = newAdviceRepository(); } return sAdviceRepository; } @Override public void addAdvice(@NonNull Stringcontent, @NonNull int uid, @NonNull final AddedAdviceCallBack callBack) { String url ="http://tr.api.gson.cn/system/feedback/" + uid; RequestParams params = newRequestParams(url); params.addBodyParameter("content", content); x.http().post(params, newCommonCallBackAdapter @Override public void onSuccess(String s) { callBack.onAdviceAdded(); } }); } @Override public void getMyAdvices(@NonNull int uid,@NonNull final LoadAdviceCallBack callBack) { String url ="http://tr.api.gson.cn/system/feedback/" + uid; RequestParams params = newRequestParams(url); x.http().get(params, newCommonCallBackAdapter @Override public voidonSuccess(List callBack.onAdviceLoaded(advices); } }); } @Override public void deleteAdvice(@NonNull int id,@NonNull int uid, @NonNull finalDeletedAdviceCallBack callBack) { String url ="http://tr.api.gson.cn/system/feedback/delete/" + uid + "/"+ id; LogUtil.d(url); RequestParams params = newRequestParams(url); x.http().get(params, newCommonCallBackAdapter @Override public void onSuccess(String s) { callBack.onAdviceDeleted(); } }); } } 因为所有的http请求都是异步请求,所以每个方法必须通过接口回调才能在请求结束后处理后续事宜。 CommonCallBackAdapter抽象类是CommonCallBack接口的适配器,后者定义了多达4个方法,但常用的只有1个,CommonCallBackAdapter类提供了另外3个不常用方法的默认实现,这样代码就简单多了。 public abstractclass CommonCallBackAdapter implementsCallback.CommonCallback @Override public void onError(Throwable throwable,boolean b) { } @Override public void onCancelled(CancelledExceptione) { } @Override public void onFinished() { } } “我的建议列表”UI中包含了下面几个功能: A) 显示我的建议; B) 菜单:提建议,将打开新的Activity; C) 菜单:刷新建议列表。后台如果回复了用户的建议,通过刷新可以马上看到回复内容。 运行效果如图5所示。 图5:我的建议列表 本UI涉及到三个布局文件:item_advices.xml、fragment_advices.xml和activity_advices.xml。其中item_advices.xml是ListView列表项的布局,fragment_advices.xml是Fragment的布局文件,包含了一个ListView组件,activity_advices.xml是Activity的布局文件,包含了一个FrameLayout(帧布局),Fragment对象将添加到FrameLayout布局中。 item_advices.xml: android:orientation="horizontal" android:padding="10dp" android:layout_width="match_parent" android:layout_height="match_parent"> android:layout_width="100dp" android:layout_height="100dp" android:id="@+id/tv_date" android:text="02/06" android:gravity="center" android:textSize="30sp" /> android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical" > android:layout_width="match_parent" android:layout_height="wrap_content" android:text="建议" android:id="@+id/tv_content" android:textSize="16dp" /> android:layout_marginTop="10dp" android:textColor="@android:color/holo_blue_dark" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:id="@+id/tv_reply" android:text="回复" /> fragment_advices.xml: android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/lv_advices"> activity_advices.xml: android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/frame_content" > 在advices包中,定义了“我的建议列表”所需要的View、Presenter和Activity,因为Model已经统一在AdviceRepository类中实现,所以不需要重复定义Model。一般来说,一个小模块定义一个对应的Model类,比如,用户模块可以将注册、登陆、修改密码、修改基本资料、注销等功能都定义在一个Model类中。 AdvicesContract接口用于集中管理View和Presenter两个接口,两个接口放在一块儿也更利用我们阅读源码。 public interfaceAdvicesContract { public interface Presenter extendsBasePresenter{ /** * 加载我的建议 */ void loadMyAdvices(); /** * 删除建议 * @param id */ void deleteAdvice(@NonNull int id); /** * 添加新建议 */ void addNewAdvice(); } public interface View extendsBaseView /** * 显示我的建议 * @param advices */ void showAdvices(List /** * 打开提建议的Activity */ void showAddAdvice(); /** * 删除我的一条建议 * @param id */ void deleteAdvice(@NonNull int id); } } AdvicesPresenter类是对AdvicesContract.Presenter接口的实现,AdvicesPresenter持有View和Model的引用,本类中分别为AdvicesContract.View和AdviceRepositorymRepository。在构造方法中不仅要建立AdvicesPresenter与AdvicesContract.View和AdviceRepository的关系,同时也要建立AdvicesContract.View与AdvicesPresenter的关系(Presenter和View是相互持有对方引用的)。 public classAdvicesPresenter implements AdvicesContract.Presenter { private AdvicesContract.View mView; private AdviceRepository mRepository; publicAdvicesPresenter(AdvicesContract.View view, AdviceRepository repository) { mView = view;//建立Presenter与View的关系 mRepository = repository; //建立View与Presenter的关系 mView.setPresenter(this); } @Override public void loadMyAdvices() { //读取id为5的用户的建议 mRepository.getMyAdvices(5, newAdviceDataSource.LoadAdviceCallBack() { @Override public voidonAdviceLoaded(List mView.showAdvices(advices); } }); } @Override public void deleteAdvice(@NonNull final intid) { //删除5号用户对应的建议 mRepository.deleteAdvice(id, 5, newAdviceDataSource.DeletedAdviceCallBack() { @Override public void onAdviceDeleted() { mView.deleteAdvice(id); } }); } @Override public void addNewAdvice() { mView.showAddAdvice(); } @Override public void start() { loadMyAdvices(); } } Presenter的start()方法通常用于初始化,本例中需要显示所有的建议列表,所以调用了loadMyAdvices()方法。 在loadMyAdvices()方法中,通过调用Model类AdviceRepository的getMyAdvices()方法获取我的建议,再调用AdvicesContract.View中的showAdvices()方法刷新列表。其他道理都差不多。再次强调一下,View将功能委托给Presenter完成,Presenter又调用View刷新结果。 AdvicesFragment类是一个继承自Fragment的View,在AdvicesFragment类中,我们早早为ListView定义了一个适配器AdviceAdapter对象,一开始什么都不显示,读取到数据后再刷新结果。在Fragment的onResume()回调方法中,通常要调用Presenter的start()方法完成初始化。以下是AdviceFragment类的源码: public classAdvicesFragment extends Fragment implements AdvicesContract.View { private AdvicesContract.PresentermPresenter; private AdviceAdapter mAdviceAdapter; public static AdvicesFragment getInstance(){ return new AdvicesFragment(); } @Override public void onCreate(@Nullable BundlesavedInstanceState) { super.onCreate(savedInstanceState); mAdviceAdapter = new AdviceAdapter(newArrayList } @Nullable @Override public View onCreateView(LayoutInflaterinflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View root =inflater.inflate(R.layout.fragment_advices, container, false); setHasOptionsMenu(true);//显示选项菜单 return root; } @Override public void onViewCreated(View view,@Nullable Bundle savedInstanceState) { ListView listView = (ListView)view.findViewById(R.id.lv_advices); listView.setOnItemLongClickListener(newAdapterView.OnItemLongClickListener() { @Override public booleanonItemLongClick(AdapterView> adapterView, View view, inti, final long l) { AlertDialog.Builder builder =new AlertDialog.Builder(getContext()); AlertDialog alertDialog =builder.setTitle("确定删除") .setMessage("确定要删除该建议吗?") .setPositiveButton("删除", new DialogInterface.OnClickListener() { @Override public voidonClick(DialogInterface dialogInterface, int i) { LogUtil.d("==建议id:" + l); mPresenter.deleteAdvice((int) l); } }) .setNegativeButton("取消", new DialogInterface.OnClickListener() { @Override public voidonClick(DialogInterface dialogInterface, int i) { } }).create(); alertDialog.show(); return true; } }); listView.setAdapter(mAdviceAdapter); } @Override public void onResume() { super.onResume(); mPresenter.start(); } @Override public void showAdvices(List mAdviceAdapter.refreshAdvices(advices); } @Override public void showAddAdvice() { Intent intent = newIntent(AddAdviceActivity.ACTION); startActivityForResult(intent, 0x002); } @Override public void onActivityResult(intrequestCode, int resultCode, Intent data) { if(requestCode == 0x002 &&resultCode == Activity.RESULT_OK) { mPresenter.loadMyAdvices(); //添加完建议后重新加载,实时将新的建议显示出来 } } @Override public void deleteAdvice(@NonNull int id) { mAdviceAdapter.deleteAdvice(id);//删除建议 } @Override public voidsetPresenter(AdvicesContract.Presenter presenter) { this.mPresenter = presenter; } @Override public void onCreateOptionsMenu(Menu menu,MenuInflater inflater) { inflater.inflate(R.menu.menu_advices,menu); } @Override public booleanonOptionsItemSelected(MenuItem item) { if(item.getItemId() ==R.id.menu_add_advice){ mPresenter.addNewAdvice(); }else if(item.getItemId() ==R.id.menu_refresh_advice){ mPresenter.loadMyAdvices();//刷新 } return true; } /** * 适配器 */ class AdviceAdapter extends BaseAdapter { private List public AdviceAdapter(List mAdvices = advices; } public voidsetAdvices(List mAdvices = advices; } @Override public int getCount() { return mAdvices.size(); } @Override public Object getItem(int i) { return mAdvices.get(i); } @Override public long getItemId(int i) { return mAdvices.get(i).getId(); } @Override public View getView(int i, View view,ViewGroup viewGroup) { View root =LayoutInflater.from(viewGroup.getContext()) .inflate(R.layout.item_advices, viewGroup, false); TextView tvDate = (TextView)root.findViewById(R.id.tv_date); TextView tvContent = (TextView)root.findViewById(R.id.tv_content); TextView tvReply = (TextView)root.findViewById(R.id.tv_reply); Advice advice = mAdvices.get(i); Calendar calendar = Calendar.getInstance(); calendar.setTime(advice.getCreateTime()); tvDate.setText(calendar.get(Calendar.MONTH) + 1 + "/" +calendar.get(Calendar.DAY_OF_MONTH)); tvContent.setText(advice.getContent()); tvReply.setText(advice.getReply()); return root; } public voidrefreshAdvices(List setAdvices(advices); notifyDataSetChanged(); } public void deleteAdvice(int id) { mPresenter.loadMyAdvices(); } } } 在Fragment中可以定义ActionBar的菜单,但默认是关闭的,需要通过setHasOptionsMenu(true)方法启用。另外,当需要改变ListView中的数据时,建议将功能封装在Adapter中。 本界面还实现了长按列表项删除建议的功能,您可以阅读源码了解方法的调用规律,基本顺序为View -> Presenter -> View。 最后是AdviceActivity类,该类继承自AppCompatActivity,在onCreate()方法中,先判断AdviceFragment对象是否存在,如果存在,就不需要创建新的AdviceFragment对象了,不存在才创建,节约内存。然后将AdviceFragment添加到Activity的FrameLayout中。最后,通过new AdvicesPresenter(fragment, AdviceRepository.getInstance())语句创建Presenter,这个语句同时创建了Model层的AdviceRepository对象。至此,Fragment(View)、Model和Presenter创建完毕,而在Presenter类中则构建了View与Presenter、Presenter与View、Presenter与Model的关系。 @ContentView(R.layout.activity_advices) public classAdviceActivity extends AppCompatActivity { private AdvicesPresenter mAdvicesPresenter; @Override protected void onCreate(@Nullable BundlesavedInstanceState) { super.onCreate(savedInstanceState); x.view().inject(this); FragmentManager fragmentManager =getSupportFragmentManager(); //判断Fragment是否存在 AdvicesFragment fragment =(AdvicesFragment) fragmentManager .findFragmentById(R.id.frame_content); //不存在则创建 if(fragment == null){ fragment =AdvicesFragment.getInstance(); ActivityUtils.addFragmentToActivity(fragmentManager, fragment,R.id.frame_content); } //创建Presenter mAdvicesPresenter = new AdvicesPresenter(fragment,AdviceRepository.getInstance()); } } 上面代码中的ActivityUtils是一个工具类,定义了一个addFragmentToActivity()方法,用于将Fragment添加到Activity中。 public classActivityUtils { public static voidaddFragmentToActivity(FragmentManager fragmentManager, Fragmentfragment, int resId){ FragmentTransaction transaction =fragmentManager.beginTransaction(); transaction.add(resId, fragment); transaction.commit(); } } 提建议就是一个添加功能,运行效果图6所示: 图6:提建议 本功能定义在addadvice包中,同样包含了View、Presenter、Contract和Activity,也就是说,每一个界面都至少需要这几个组件。本功能包含了fragment_add_advice.xml和activity_add_advice.xml两个布局文件,前者为Fragment定义了界面,后者为Activity定义界面。 fragment_add_advice.xml: android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> android:layout_width="match_parent" android:layout_height="300px" android:hint="请输入您宝贵的建议" android:padding="10dp" android:gravity="left|top" android:id="@+id/et_content" /> 4.4 定义Model
>() {
4.5 我的建议列表功能实现
4.6 提建议功能实现