您的位置:首页 > 移动开发

Android MVP模式实战练习之一步一步打造一款简易便笺app(一)

2017-07-15 18:11 447 查看

介绍

相信做开发的我们,都是知道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的具体实现。

源码地址:https://github.com/CCY0122/MVP-Note_app

下文链接:Android MVP模式实战练习之一步一步打造一款简易便笺app(二)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息