Android MVP模式实战练习之一步一步打造一款简易便笺app(一)
来源:互联网 发布:win10网络连接红叉 编辑:程序博客网 时间:2024/06/01 22:30
介绍
相信做开发的我们,都是知道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(二)
- Android MVP模式实战练习之一步一步打造一款简易便笺app(一)
- Android MVP模式实战练习之一步一步打造一款简易便笺app(二)
- 打造Android MVP模式(一)
- 如何打造一款 android app
- android之MVP模式(一)
- Android MVP模式实战
- Android MVP模式实战
- Android开源实战:使用MVP+Retrofit开发一款文字阅读APP
- 打造Android的MVP模式
- Android 设计模式之 MVP(一)
- Android MVP模式之(一)初识
- android 用mvp模式来架构自己的app+打造Recyclerview万能适配器
- Android之MVP模式
- Android 之MVP模式
- Android开发模式之--MVP设计模式一
- 如何优雅的使用Retrofit、Rxjava、Butterknife、Material开发一款MVP模式的新闻+天气预报+妹子的Android app
- MVP+Databinding模式开发APP(一)
- 构建一款App之使用设计模式
- 进程虚拟地址空间
- linux中socket编程出现 connect: No route to host
- 我的第一篇博客的尝试
- 缘分,来过便已成诗
- sublime-text3打造markdown编辑器
- Android MVP模式实战练习之一步一步打造一款简易便笺app(一)
- POJ Protecting the Flowers
- django中一个应用使用另一个应用的模型类并建立外键
- opencv配置:opencv3.2.0+VS2017
- NAT技术基本知识
- JavaWeb学习十八(util包下的Date和sql包下的Date的转换)
- 一个很智障却很实用的程序(十进制转二进制)
- 根据另外一张表更新已有表字段
- 欢迎使用CSDN-markdown编辑器