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


这里写图片描述
这里写图片描述
这里写图片描述
图1

图2

通过上面演示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是卡片布局

阅读建议

  1. 保持面向接口编程的思想,V和P、M和P之间都是通过接口来联系的。
  2. MVP模式是适用于较大的项目的,我们这个便笺app是相当相当简单的,我们本来就是为了练习MVP架构而UI从简,所以大家阅读过程中没必要有“这里明明一两行代码/一两个方法就能实现了,干嘛非要写的这么复杂”,就是假设了每一步操作都涉及复杂逻辑,故意这么写的。
  3. 耐心,这不像写自定义view那样,可以写一点看一点,我们得把整个框架全写好了,才能运行看效果
  4. 文中说的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(二)

原创粉丝点击