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

介绍

相信做开发的我们,都是知道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(二)

猜你喜欢

转载自blog.csdn.net/ccy0122/article/details/75175203