介绍
相信做开发的我们,都是知道MVP模式的,该模式将提供数据的(M)Model和显示视图的(V)View互相隔离,使用(P)Presenter作为控制层来联系M和V。介绍MVP的文章也是相当多的,不过还是自己动手写一写收获更大。本文便是使用mvp模式一步一步去打造一款简易的便笺app
谷歌为了让我们能好好学习mvp模式,出品了一个开源项目android-architecture,该项目使用了不同变体的mvp模式来编写同一个名为todoapp的项目,其接近20K的star足以证明它的学习价值。本项目也是以它最基本的todoapp作为学习模板,整体架构保持一致,但是没有像它那样还编写了各种单元测试、UI测试、自动化测试的代码和依赖
项目演示
本项目源码地址:
便笺 MVP-Note_app
通过上面演示gif图,可以看到本项目有两个界面:列表界面、编辑界面。
- 列表界面
- 列表界面展示所有便笺,并且每个便笺可以标记为已完成和未完成的状态
- 侧滑列表可以筛选便笺,也可以删除已完成的便笺
- 单击便笺进入编辑界面,长按便笺可删除该便笺
- 点击toolbar上刷新图标即刷新,点击右下角fab即创建便笺
- 编辑界面
- 有标题和内容两块编辑区域
- 点击toolbar上的删除选项即删除当前便笺,点击右下角fab即保存便笺
看起来功能就这么一点,那么我们再看看该项目结构目录:
还是不少的(MVP的一个缺点就是会使类的数量增加许多)。先简单介绍下:
- data包顾名思义是提供数据的,即M
- editnote包即编辑界面相关的V和P
- notelist包即列表界面相关的V和P
- util即工具类,BasePresenter即基类P,BaseView即基类V
本项目不会对UI过多介绍,读者至少需知道DrawerLayout是侧滑菜单布局,NavigationView是material design风格菜单,SwipeRefreshLayout是material design风格的下拉刷新控件,toolbar是标题栏,FloatingActionButton是悬浮按钮,CardView是卡片布局
阅读建议
- 保持面向接口编程的思想,V和P、M和P之间都是通过接口来联系的。
- MVP模式是适用于较大的项目的,我们这个便笺app是相当相当简单的,我们本来就是为了练习MVP架构而UI从简,所以大家阅读过程中没必要有“这里明明一两行代码/一两个方法就能实现了,干嘛非要写的这么复杂”,就是假设了每一步操作都涉及复杂逻辑,故意这么写的。
- 耐心,这不像写自定义view那样,可以写一点看一点,我们得把整个框架全写好了,才能运行看效果
- 文中说的View都是指MVP中的V,而不是系统控件view
开始编写
设计用于提供数据的Model接口
第一步我们先设计好提供并存储我们数据的接口应该是怎样的,因为后面各个界面的Presenter都需要通过该接口来获取数据。
首先创建一个便笺bean,以id作为其唯一标示,如下所示:
public class NoteBean {
public String id;
public String title;
public String content;
public boolean isActive;
public NoteBean(String title, String content, boolean isActive) {
this.id = UUID.randomUUID().toString(); //保证id唯一性
this.title = title;
this.content = content;
this.isActive = isActive;
}
public NoteBean(String id, String title, String content, boolean isActive) {
this.id = id;
this.title = title;
this.content = content;
this.isActive = isActive;
}
}
接下来创建一个数据接口NoteDataSource,通过分析我们的便笺app有哪些是涉及到数据存储的,可以拟出该接口定义的方法如下:
/**
* Created by ccy on 2017-07-12.
* MVP之Model
* 简单起见,只有获取数据有回调
* 实际上删除、修改等数据操作也应该有回调
*/
public interface NoteDataSource {
/**
* 获取单个数据的回调
*/
interface LoadNoteCallback{
void loadSuccess(NoteBean note);
void loadFailed();
}
/**
* 获取全部数据的回调
*/
interface LoadNotesCallback{
void loadSucess(List<NoteBean> notes);
void loadFailed();
}
void getNote(String noteId,LoadNoteCallback callback); //通过id获取指定数据
void getNotes(LoadNotesCallback callback); //获取所有数据
void saveNote(NoteBean note);
void updateNote(NoteBean note);
void markNote(NoteBean note,boolean isActive); //标记便笺完成状态
void clearCompleteNotes();
void deleteAllNotes();
void deleteNote(String noteId);
void cacheEnable(boolean enable); //缓存是否可用(如果有)
}
以上定义的方法通过其名称应该都能知道他的作用。接下来我们创建它的实现类NotesRepository,它负责着与各界面的Presenter之间进行通信:
**
* Created by ccy on 2017-07-12.
* MVP之Model实现类
* 管理数据处理
* 单例
*/
public class NotesRepository implements NoteDataSource {
private NotesRepository(NoteDataSource notesLocalDataSource){
}
public static NotesRepository getInstence(){
if(INSTANCE == null){
INSTANCE = new NotesRepository();
}
return INSTANCE;
}
@Override
public void getNote(final String noteId, final LoadNoteCallback callback) {
}
@Override
public void getNotes(final LoadNotesCallback callback) {
}
@Override
public void saveNote(NoteBean note) {
}
@Override
public void updateNote(NoteBean note) {
}
@Override
public void markNote(NoteBean note, boolean isActive) {
}
@Override
public void clearCompleteNotes() {
}
@Override
public void deleteAllNotes() {
}
@Override
public void deleteNote(String noteId) {
}
@Override
public void cacheEnable(boolean enable) {
}
}
它是一个单例,暂时是个空壳,具体方法实现呢我们之后再去写,目前我们着眼与整体流程的编写。
编写便笺列表界面View和Presenter
首先要说明我们每个界面都有着以下特征:
- 一个Activity,它管理着最基础的布局和创建V和P的任务
- 一个Fragment,它是Activity里主要的布局,扮演View的角色
- 一个Presenter类,它扮演Presenter角色
- 一个Contract类,它管理着当前界面的View和Presenter的接口定义
好了,我们首先要给MainActivity写一个xml布局。先直接看下代码:
layout/main_act:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.ccy.mvp_note.notelist.MainActivity">
<!--主界面-->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorPrimary"
android:paddingTop="25dp"
></android.support.v7.widget.Toolbar>
</android.support.design.widget.AppBarLayout>
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/fragment_content"
android:layout_width="match_parent"
android:layout_height="match_parent">
</FrameLayout>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:fabSize="normal"
app:layout_anchor="@id/fragment_content"
app:layout_anchorGravity="end|bottom"
android:src="@drawable/add"/>
</android.support.design.widget.CoordinatorLayout>
</LinearLayout>
<!--菜单界面-->
<android.support.design.widget.NavigationView
android:id="@+id/navigation_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:headerLayout="@layout/nav_header"
app:menu="@menu/nav_menu"></android.support.design.widget.NavigationView>
</android.support.v4.widget.DrawerLayout>
可以看到根布局是一个DrawerLayout,即菜单布局,他包含两个子布局:
第一个子布局即主界面,它里面有一个被AppBarLayout包裹着的标题栏ToolBar,和一个被CoordinatorLayout包裹着的FrameLayout和fab(ps:为了方便,文中用fab表示FloatingActionButton),这个FrameLayout就是用来放我们后面的fragment用的。
如果你不知道AppBarLayout、CoordinatorLayout,那直接无视掉就好,他们是一个大知识点,不可能在本文中讲解的,而且UI不是本项目重点,你可以把他们当成FrameLayout就好。
第二个子布局即菜单界面,它是一个NavigationView,其内部通过
app:headerLayout
指明菜单头部布局,通过 app:menu
指明菜单布局。观察项目截图可知,头部布局就是一张海贼王的图片,菜单也只有4个item,我们快速过一下他俩的代码:
layout/nav_header:
<?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="200dp">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/q"
android:scaleType="centerCrop"/>
</LinearLayout>
menu/nav_header:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_filter_all"
android:icon="@drawable/menu"
android:title="全部便笺"
android:checkable="true"/>
<item
android:id="@+id/menu_filter_active"
android:icon="@drawable/active"
android:title="未完成的"
android:checkable="true"/>
<item
android:id="@+id/menu_filter_complete"
android:icon="@drawable/complete"
android:title="已完成的"
android:checkable="true"/>
<item android:id="@+id/menu_clear_complete"
android:icon="@drawable/delete"
android:title="删除已完成的"/>
</menu>
接下来将该布局设置给MainActivity,并在里面创建好V和P,代码如下:
MainActivity:
public class MainActivity extends AppCompatActivity {
private Toolbar toolbar;
private DrawerLayout drawerLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_act);
//5.0以上使布局延伸到状态栏的方法
View decorView = getWindow().getDecorView();
int option = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN|View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
decorView.setSystemUiVisibility(option);
getWindow().setStatusBarColor(Color.TRANSPARENT);
//初始化toolBar、drawerLayout
toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
ActionBar ab = getSupportActionBar();
ab.setHomeAsUpIndicator(R.drawable.ic_menu); //设置toolbar最左侧图标(id为android.R.id.home),默认是一个返回箭头
ab.setDisplayHomeAsUpEnabled(true);//设置是否显示左侧图标
drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
//创建fragment (V)
MainFragment mainFragment = (MainFragment) getSupportFragmentManager().findFragmentById(R.id.fragment_content);
if(mainFragment == null){
mainFragment = MainFragment.newInstence();
ActivityUtils.addFragmentToActivity(getSupportFragmentManager(),mainFragment,R.id.fragment_content);
}
//创建Presenter (P)
MainPresenter mainPresenter = new MainPresenter(Injection.provideRespository(this),mainFragment);
}
//还须重写onCreateOptionsMenu,该方法写在fragment里
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()){
case android.R.id.home:
drawerLayout.openDrawer(GravityCompat.START);
break;
}
return super.onOptionsItemSelected(item);
}
}
可以看到,在MainActivity里初始化了ToolBar和drawerLayout,然后就是最关键的创建了MainFragment (V)和MainPresenter (P),视图逻辑都在MainFragment 里面,涉及数据操作的逻辑都在MainPresenter 里面,他俩是我们接下来的重点。
创建MainFragment 继承于Fragment,我们首先去完成它基本的视图,通过截图可知,他其实就是以SwipeRefreshLayout 作为根布局,内容由一个头部TextView和一个RecyclerView组成。我们过一眼他的xml:
layout/main_frag:
<android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/swipe_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#F2F2F2">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/header_tv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:padding="5dp"
android:textSize="26sp"
android:text="没有便笺,请创建" />
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"></android.support.v7.widget.RecyclerView>
</LinearLayout>
</android.support.v4.widget.SwipeRefreshLayout>
将这个布局设置给MainFragment 并初始化它的界面,我们先直接看下他初始代码:
MainFragment:
public class MainFragment extends Fragment {
private RecyclerView recyclerView;
private SwipeRefreshLayout swipeRefreshLayout;
private NavigationView navigationView;
private FloatingActionButton fab;
private TextView headerView;
private RecyclerAdapter adapter;
private List<NoteBean> data = new ArrayList<>();
public static MainFragment newInstence(){
return new MainFragment();
}
@Override
public void onResume() {
super.onResume();
//todo:初始化
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.main_frag,container,false);
//初始化view
headerView = (TextView) v.findViewById(R.id.header_tv);
swipeRefreshLayout = (SwipeRefreshLayout) v.findViewById(R.id.swipe_refresh);
fab = (FloatingActionButton) getActivity().findViewById(R.id.fab);
navigationView = (NavigationView) getActivity().findViewById(R.id.navigation_view);
recyclerView = (RecyclerView) v.findViewById(R.id.recycler_view);
adapter = new RecyclerAdapter(data, onNoteItemClickListener);
GridLayoutManager gridLayoutManager = new GridLayoutManager(getContext(),2);
recyclerView.setLayoutManager(gridLayoutManager);
recyclerView.setAdapter(adapter);
swipeRefreshLayout.setColorSchemeColors( //设置刷新时颜色动画,第一个颜色也会应用于下拉过程中的颜色
ContextCompat.getColor(getActivity(), R.color.colorPrimary),
ContextCompat.getColor(getActivity(), R.color.colorAccent),
ContextCompat.getColor(getActivity(), R.color.colorPrimaryDark)
);
swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
//todo:加载数据
}
});
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//todo:创建便笺
}
});
setupNavigationView(navigationView);
//使fragment参与对menu的控制(使onCreateOptionsMenu、onOptionsItemSelected有效)
setHasOptionsMenu(true);
return v;
}
private void setupNavigationView(NavigationView navigationView) {
navigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()){
case R.id.menu_filter_all:
//todo:显示全部便笺
break;
case R.id.menu_filter_active:
//todo:显示未完成的便笺
break;
case R.id.menu_filter_complete:
//todo:显示已完成的便笺
break;
case R.id.menu_clear_complete:
//todo:删除已完成的便笺
break;
}
((DrawerLayout)getActivity().findViewById(R.id.drawer_layout)).closeDrawer(GravityCompat.START);
return true;
}
});
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.main_menu,menu);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()){
case R.id.refresh:
//todo:加载数据
break;
}
return true;
}
/**
* RecyclerView的点击事件监听
*/
RecyclerAdapter.OnNoteItemClickListener onNoteItemClickListener = new RecyclerAdapter.OnNoteItemClickListener() {
@Override
public void onNoteClick(NoteBean note) {
//todo:编辑便笺
}
@Override
public void onCheckChanged(NoteBean note, boolean isChecked) {
if(isChecked){
//todo:标记便笺为已完成
}else{
//todo:标记便笺为未完成
}
}
@Override
public boolean onLongClick(View v, final NoteBean note) {
final AlertDialog dialog;
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder.setMessage("确定要删除么?");
builder.setTitle("警告");
builder.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
//todo:删除便笺
}
});
builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
dialog = builder.create();
dialog.show();
return true;
}
};
menu/main_menu:
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/refresh"
android:icon="@drawable/refresh"
android:title="刷新"
app:showAsAction="always"/>
</menu>
通过以上代码:
1.首先可以看到,我们给RecyclerView设置了一个两列的GridLayoutManager ,并以List < NoteBean > data 作为数据源设置了一个adapter。这些是RecyclerView的基础知识,就不解释了。
附上adapter和item的代码:
RecyclerAdapter:
public class RecyclerAdapter extends RecyclerView.Adapter<RecyclerAdapter.ViewHolder> {
private List<NoteBean> data;
private OnNoteItemClickListener listener;
public RecyclerAdapter(List<NoteBean> data, OnNoteItemClickListener l){
this.data = data;
this.listener = l;
}
//更换数据
public void replaceData(List<NoteBean> data){
this.data = data;
notifyDataSetChanged();
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.main_rv_item,parent,false);
return new ViewHolder(v);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
NoteBean bean = data.get(position);
holder.checkBox.setChecked(!bean.isActive);
holder.title.setText(bean.title+"");
holder.content.setText(bean.content+"");
initListener(holder,position);
}
private void initListener(final ViewHolder vh,final int pos) {
if(listener != null){
vh.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
listener.onNoteClick(data.get(pos));
}
});
vh.itemView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
return listener.onLongClick(v,data.get(pos));
}
});
//一个坑:不要使用setOnCheckedChangeListener,这个监听会在每次绑定item时就调用一次
vh.checkBox.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
listener.onCheckChanged(data.get(pos),vh.checkBox.isChecked());
}
});
}
}
@Override
public int getItemCount() {
return data.size();
}
class ViewHolder extends RecyclerView.ViewHolder{
private TextView title;
private TextView content;
private CheckBox checkBox;
public ViewHolder(View itemView) {
super(itemView);
title = (TextView) itemView.findViewById(R.id.title);
content = (TextView)itemView.findViewById(R.id.content);
checkBox = (CheckBox) itemView.findViewById(R.id.checkbox);
}
}
interface OnNoteItemClickListener {
/**
* item点击回调
* @param note
*/
void onNoteClick(NoteBean note);
/**
* checkBox点击回调
* @param note
* @param isChecked
*/
void onCheckChanged(NoteBean note,boolean isChecked);
/**
*长按回调
* @param note
* @return 是否消费
*/
boolean onLongClick(View v,NoteBean note);
}
}
layout/main_rv_item:
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
android:layout_marginBottom="6dp"
android:layout_marginTop="6dp"
android:orientation="vertical"
app:cardBackgroundColor="#FFFFFF"
app:cardCornerRadius="4dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<CheckBox
android:id="@+id/checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:padding="6dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="6dp">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="22sp"
android:lines="1"
android:ellipsize="end"/>
<TextView
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:lines="1"
android:paddingLeft="10dp"
android:paddingTop="6dp"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
</android.support.v7.widget.CardView>
2.另外我们可以看到的是所有布局的点击等操作都还是空的,只是留下了一个//todo的注释,因为这些操作逻辑并不由V管理,而是由P来管理的,我们目前要做的只是去考虑这个V都有哪些跟视图显示有关的逻辑,这是解耦的关键。
那么根据面向接口编程思想,不用说,我们现在要为这个Fragment设计一个V的接口,创建MainContract类:
MainContract:
public class MainContract {
interface View extends BaseView<Presenter>{
}
interface Presenter extends BasePresenter{
}
}
这个类是管理当前界面的V和P的,可以看到声明了两个接口,他俩继承的基础接口代码如下:
/**
* MVP中的基础V
* @param <T>
*/
public interface BaseView<T> {
void setPresenter(T presenter);
}
/**
* MVP中的基础P
*/
public interface BasePresenter {
void start();
}
接下来我们仔细想想,当前这个便笺列表都有哪些跟视图显示相关的逻辑呢?由于我们现在假设了这个项目是一个很大很复杂的项目,因此我们将显示逻辑想的非常细,然后给接口View设计了如下这么多的方法:
public class MainContract {
interface View extends BaseView<Presenter>{
void setLoadingIndicator(boolean active); //显示、隐藏加载控件
void showNotes(List<NoteBean> notes); //显示便笺
void showLoadNotesError();//加载便笺失败
void showAddNotesUi(); //显示创建便笺界面
void showNoteDetailUi(String noteId); //显示编辑便笺界面
void showAllNoteTip();//以下4个方法对应各种状态下需显示的内容
void showActiveNoteTip();
void showCompletedNoteTip();
void showNoNotesTip();
void showNoteDeleted(); //删除了一个便笺后
void showCompletedNotesCleared();//删除了已完成的便笺后
void showNoteMarkedActive();//有便笺被标记为未完成后
void showNoteMarkedComplete();//有便笺被标记为已完成后
boolean isActive(); //用于判断当前界面是否还在前台
}
interface Presenter extends BasePresenter{
}
}
可以说是相当多了,当然,你一下子可能想不全,没关系,反正就是一个接口,等后面想到了再为其添加也是很正常的。
这里重点提一下 boolean isActive();
这个接口方法,他是用于判断当前界面还是不是在前台的,因为实际项目中我们去获取某个数据时,都是一个耗时、异步的过程,那么当数据获取完毕并调用了回调时,原先发起数据请求的那个界面有可能已经不在前台了,那就没必要再执行显示逻辑了,所以我们为其添加了 boolean isActive()
这么一个方法。
V的接口设计好了,接下来就是让我们的MainFragment作为它的实现类,实现它的所有方法:
MainFragment:
public class MainFragment extends Fragment implements MainContract.View {
private MainContract.Presenter presenter; //View持有Presenter
private RecyclerView recyclerView;
private SwipeRefreshLayout swipeRefreshLayout;
private NavigationView navigationView;
private FloatingActionButton fab;
private TextView headerView;
private RecyclerAdapter adapter;
private List<NoteBean> data = new ArrayList<>();
//……………………省略已有代码
//以下为MainContract.View接口实现
@Override
public void setPresenter(MainContract.Presenter presenter) {
this.presenter = presenter;
}
@Override
public void setLoadingIndicator(final boolean active) {
if(getView() == null){
return;
}
//用post可以保证swipeRefreshLayout已布局完成
swipeRefreshLayout.post(new Runnable() {
@Override
public void run() {
swipeRefreshLayout.setRefreshing(active);
}
});
}
@Override
public void showNotes(List<NoteBean> notes) {
adapter.replaceData(notes);
}
@Override
public void showLoadNotesError() {
Snackbar.make(getView(),"加载数据失败",Snackbar.LENGTH_LONG).show();
}
@Override
public void showAddNotesUi() {
Intent i = new Intent(getActivity(), EditActivity.class);
startActivity(i);
}
@Override
public void showNoteDetailUi(String noteId) {
Intent i = new Intent(getActivity(),EditActivity.class);
i.putExtra(EditActivity.EXTRA_NOTE_ID,noteId);
startActivity(i);
}
@Override
public void showAllNoteTip() {
headerView.setBackgroundColor(0x88ff0000);
headerView.setText("全部便笺");
}
@Override
public void showActiveNoteTip() {
headerView.setBackgroundColor(0x8800ff00);
headerView.setText("未完成的便笺");
}
@Override
public void showCompletedNoteTip() {
headerView.setBackgroundColor(0x880000ff);
headerView.setText("已完成的便笺");
}
@Override
public void showNoNotesTip() {
headerView.setBackgroundColor(0xffffffff);
headerView.setText("没有便笺,请创建");
}
@Override
public void showNoteDeleted() {
Snackbar.make(getView(),"成功删除该便笺",Snackbar.LENGTH_LONG).show();
}
@Override
public void showCompletedNotesCleared() {
Snackbar.make(getView(),"成功清除已完成便笺",Snackbar.LENGTH_LONG).show();
}
@Override
public void showNoteMarkedActive() {
Snackbar.make(getView(),"成功标记为未完成",Snackbar.LENGTH_LONG).show();
}
@Override
public void showNoteMarkedComplete() {
Snackbar.make(getView(),"成功标记为已完成",Snackbar.LENGTH_LONG).show();
}
@Override
public boolean isActive() {
return isAdded(); //判断当前Fragment是否添加至Activity
}
}
好了,到此,我们的V算是设计好了,他只负责了视图显示相关的逻辑,接下来我们就要设计P了,他的同时持有V和M,为视图显示和数据操作建立其联系的桥梁。
我们先回到MainFragment中,看看我们之前留下的//todo注释,一共有以下这么多:
- //todo:初始化
- //todo:加载数据
- //todo:创建便笺
- //todo:显示全部便笺
- //todo:显示未完成的便笺
- //todo : 显示已完成的便笺
- //todo : 删除已完成的便笺
- //todo : 编辑便笺
- //todo : 标记便笺为已完成
- //todo : 标记标记为未完成
//todo : 删除便笺
这些可以说是我们该界面全部的“业务逻辑”了,根据这些业务逻辑,我们可以很容易的设计出P接口该有哪些方法:
public class MainContract {
interface View extends BaseView<Presenter>{
//………………省略已有代码
}
interface Presenter extends BasePresenter{
/**
*加载便笺数据
* @param forceUpdate 是否是更新。true则从数据源(服务器、数据库等)获取数据,false则从缓存中直接获取
* @param showLoadingUI 是否需要显示加载框
*/
void loadNotes(boolean forceUpdate,boolean showLoadingUI);
void addNote(); //添加便笺
void deleteNote(NoteBean bean); //删除便笺
void openNoteDetail(NoteBean bean); //便笺详情
void makeNoteComplete(NoteBean bean); // 标记便笺为已完成
void makeNoteActive(NoteBean bean); //标记便笺为未完成
void clearCompleteNotes(); //清除已完成便笺
void setFiltering(FilterType type); //数据过滤
}
}
上述接口中需要注意一下的是void loadNotes(boolean forceUpdate,boolean showLoadingUI);
它的第一个参数,为true表示从数据源重新加载数据,为false时只是从缓存里直接取出数据;
void setFiltering(FilterType type);
这个方法要传的参数是一个枚举,如下所示:
public enum FilterType {
/**
* 全部便笺
*/
ALL_NOTES,
/**
* 未完成的便笺
*/
ACTIVE_NOTES,
/**
* 已完成的便笺
*/
COMPLETED_NOTES,
}
接口已经设计好了,我们先不着急创建它的实现类,我们先让MainFragment持有这个接口,并把接口方法放到对应的//todo注释处。这样我们这个MainFragment已经是一个完整的V了,他完成了自己所有跟显示有关的逻辑,并将自己所有跟操作有关的逻辑交给了P,这个时候解耦的感觉就粗来啦。
代码如下:
MainFrgment
public class MainFragment extends Fragment implements MainContract.View {
private MainContract.Presenter presenter; //View持有Presenter
private RecyclerView recyclerView;
private SwipeRefreshLayout swipeRefreshLayout;
private NavigationView navigationView;
private FloatingActionButton fab;
private TextView headerView;
private RecyclerAdapter adapter;
private List<NoteBean> data = new ArrayList<>();
public static MainFragment newInstence(){
return new MainFragment();
}
@Override
public void onResume() {
super.onResume();
presenter.start();
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.main_frag,container,false);
//初始化view
headerView = (TextView) v.findViewById(R.id.header_tv);
swipeRefreshLayout = (SwipeRefreshLayout) v.findViewById(R.id.swipe_refresh);
fab = (FloatingActionButton) getActivity().findViewById(R.id.fab);
navigationView = (NavigationView) getActivity().findViewById(R.id.navigation_view);
recyclerView = (RecyclerView) v.findViewById(R.id.recycler_view);
adapter = new RecyclerAdapter(data, onNoteItemClickListener);
GridLayoutManager gridLayoutManager = new GridLayoutManager(getContext(),2);
recyclerView.setLayoutManager(gridLayoutManager);
recyclerView.setAdapter(adapter);
swipeRefreshLayout.setColorSchemeColors( //设置刷新时颜色动画,第一个颜色也会应用于下拉过程中的颜色
ContextCompat.getColor(getActivity(), R.color.colorPrimary),
ContextCompat.getColor(getActivity(), R.color.colorAccent),
ContextCompat.getColor(getActivity(), R.color.colorPrimaryDark)
);
swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
presenter.loadNotes(true,true);
}
});
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
presenter.addNote();
}
});
setupNavigationView(navigationView);
//使fragment参与对menu的控制(使onCreateOptionsMenu、onOptionsItemSelected有效)
setHasOptionsMenu(true);
return v;
}
private void setupNavigationView(NavigationView navigationView) {
navigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()){
case R.id.menu_filter_all:
presenter.setFiltering(FilterType.ALL_NOTES);
break;
case R.id.menu_filter_active:
presenter.setFiltering(FilterType.ACTIVE_NOTES);
break;
case R.id.menu_filter_complete:
presenter.setFiltering(FilterType.COMPLETED_NOTES);
break;
case R.id.menu_clear_complete:
presenter.clearCompleteNotes();
break;
}
presenter.loadNotes(false,false); //参数为false,不需要从数据源重新获取数据,从缓存取出并过滤即可,也没必要显示加载条
((DrawerLayout)getActivity().findViewById(R.id.drawer_layout)).closeDrawer(GravityCompat.START);
return true;
}
});
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.main_menu,menu);
super.onCreateOptionsMenu(menu, inflater);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()){
case R.id.refresh:
presenter.loadNotes(true,true);
break;
}
return true;
}
/**
* RecyclerView的点击事件监听
*/
RecyclerAdapter.OnNoteItemClickListener onNoteItemClickListener = new RecyclerAdapter.OnNoteItemClickListener() {
@Override
public void onNoteClick(NoteBean note) {
presenter.openNoteDetail(note);
}
@Override
public void onCheckChanged(NoteBean note, boolean isChecked) {
if(isChecked){
presenter.makeNoteComplete(note);
}else{
presenter.makeNoteActive(note);
}
}
@Override
public boolean onLongClick(View v, final NoteBean note) {
final AlertDialog dialog;
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder.setMessage("确定要删除么?");
builder.setTitle("警告");
builder.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
presenter.deleteNote(note);
}
});
builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
dialog = builder.create();
dialog.show();
return true;
}
};
//以下为MainContract.View接口实现
@Override
public void setPresenter(MainContract.Presenter presenter) {
this.presenter = presenter;
}
//…………省略其他剩余的接口实现方法
}
接下来创建P的实现类MainPresenter,他同时持有V和M,是任务最艰巨难度最大的以角色。我们V和M的接口在上面都已经设计好了,还是先直接贴上完整的代码
MainPresenter:
public class MainPresenter implements MainContract.Presenter {
private MainContract.View notesView; //Presenter持有View
private NotesRepository notesRepository; //MVP的Model,管理数据处理
private FilterType filterType = FilterType.ALL_NOTES; //当前过滤条件
private boolean isFirstLoad = true;
public MainPresenter(NotesRepository notesRepository, MainContract.View notesView) {
this.notesView = notesView;
this.notesRepository = notesRepository;
notesView.setPresenter(this); //重要!别落了
}
//以下为MainContract.Presenter接口实现
@Override
public void start() {
if (isFirstLoad) {
loadNotes(true, true); //第一次打开界面时从数据源获取数据
isFirstLoad = false;
} else {
loadNotes(false, true);
}
}
@Override
public void loadNotes(boolean forceUpdate, final boolean showLoadingUI) {
if (showLoadingUI) {
notesView.setLoadingIndicator(true);
}
notesRepository.cacheEnable(forceUpdate);
notesRepository.getNotes(new NoteDataSource.LoadNotesCallback() {
@Override
public void loadSucess(List<NoteBean> notes) {
if (showLoadingUI) {
notesView.setLoadingIndicator(false);
}
List<NoteBean> notesToShow = new ArrayList<NoteBean>();
//根据当前过滤条件来过滤数据
for (NoteBean bean : notes) {
switch (filterType) {
case ALL_NOTES:
notesToShow.add(bean);
break;
case ACTIVE_NOTES:
if (bean.isActive) {
notesToShow.add(bean);
}
break;
case COMPLETED_NOTES:
if (!bean.isActive) {
notesToShow.add(bean);
}
break;
}
}
//即将显示数据了,先判断一下持有的View还在不在前台
if (!notesView.isActive()) {
return; //没必要显示了
}
switch (filterType) {
case ALL_NOTES:
notesView.showAllNoteTip();
break;
case ACTIVE_NOTES:
notesView.showActiveNoteTip();
break;
case COMPLETED_NOTES:
notesView.showCompletedNoteTip();
break;
}
if (notesToShow.isEmpty()) {
notesView.showNoNotesTip();
notesView.showNotes(notesToShow);
} else {
notesView.showNotes(notesToShow);
}
}
@Override
public void loadFailed() {
if (!notesView.isActive()) {
return;
}
if (showLoadingUI) {
notesView.setLoadingIndicator(false);
}
notesView.showLoadNotesError();
}
});
}
@Override
public void addNote() {
notesView.showAddNotesUi();
}
@Override
public void deleteNote(NoteBean bean) {
notesRepository.deleteNote(bean.id);
notesView.showNoteDeleted();
loadNotes(false,false);
}
@Override
public void openNoteDetail(NoteBean bean) {
notesView.showNoteDetailUi(bean.id);
}
@Override
public void makeNoteComplete(NoteBean bean) {
notesRepository.markNote(bean, false);
notesView.showNoteMarkedComplete();
if(filterType != FilterType.ALL_NOTES){
loadNotes(false,false);
}
}
@Override
public void makeNoteActive(NoteBean bean) {
notesRepository.markNote(bean, true);
notesView.showNoteMarkedActive();
if(filterType != FilterType.ALL_NOTES){
loadNotes(false,false);
}
}
@Override
public void clearCompleteNotes() {
notesRepository.clearCompleteNotes();
notesView.showCompletedNotesCleared();
loadNotes(false, false);
}
@Override
public void setFiltering(FilterType type) {
this.filterType = type;
}
}
可以看到,在初始的start()
方法里,如果是第一次加载,就调用loadNotes(true,true)
,否则就调用loadNotes(false,true)
,前者表示从数据源获取数据,后者表示从缓存中获取数据。
再来看看loadNotes方法,通过notesRepository.cacheEnable(forceUpdate);
来设置缓存是否可用,这样我们就告诉M要不要从缓存读取数据了,具体M怎么去实现这个逻辑P表示我才不管。然后就是调用了notesRepository.getNotes
去获取全部的便笺数据,在其回调里,我们根据当前过滤条件来筛选了一下数据,然后使用了这么一个判断:if (!notesView.isActive()) {return; }
即如果持有的V已经不在前台了,那就直接结束掉,否则,我们就根据具体情况去调用V对应的方法。
其他的方法就都比较简单了,基本就是在根据具体情况去组合一下V接口和M接口中对应的方法。请大家好好理解一下。
保持住面向接口编程 和解耦的想法,不要有一看到某某接口回调就强迫症的想去找他的实现类,这样容易被绕晕的。
到此为止便笺列表的V和P已经完全写好了,虽然M的具体实现类(NotesRepository)还是个空壳,但是我们已经将他与V完全隔离开了,V表示我才无所谓你这个提供数据的M是怎么实现的,老子已经把自己该做的事全做好了。你看这里已经体现出MVP的优点了,解耦使得我们可以在没有具体数据的情况下写好界面(反之亦然),这在我们实际工作中就是可以不等后端做好数据接口或是提供.so库的情况下就预先编写界面逻辑,可以提高不少效率哦。
休息一下吧。下一篇继续完成编辑便笺界面和M的具体实现。