Android程序设计探索:MVP与模块化

作者: Benhero | 来源:发表于2017-06-29 16:21 被阅读2715次

    一. MVP

    0. 背景

    最早接触到MVP这种设计模式,是在14年读
    《打造高质量Android应用:Android开发必知的50个诀窍》一书中了解到,而之后也逐步尝试去使用,至今体验下来,它不是一个可以完美到可以生搬硬套到各个场景的模式,正确地使用才能最好地发挥它的作用。

    1. 作用简介

    • 分层:将代码分层,抽取出数据、模型、界面。
    • 复用:对V层或者P层接口的多种实现。

    2. 作用-分层

    我们大部分对MVP着迷的一个原因是早期写业务复杂的Activity时,代码量过于庞大,导致可读性很差。
    而MVP通过3层的分离,有效地减少了Activity的代码量。
    对于这个作用的理解上,个人认为,只有代码量比较大(大于1000行),并且Activity内各个功能模块比较耦合的时候,适用MVP模式。

    3. 作用-复用

    这是MVP的另一个非常优雅的使用场景。

    • 当需要实现多个布局界面,但业务逻辑却不相同的场景时(即一个V层对应多个P层),MVP非常适用。
    • 当然,多个布局架构不一致,但业务逻辑一致的情况(即一个P层对应多个V层),MVP也适用,不过至今我还遇到这种情况。

    以下举个案例:
    需求是实现多个以下的界面,布局架构一致,但数据内容、触发逻辑都不相同。

    image.png

    代码目录层次

    image.png

    ①. V层

    package com.benhero.design.mvp.view;
    
    import android.content.Intent;
    import android.os.Bundle;
    import android.support.v7.app.AppCompatActivity;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.BaseAdapter;
    import android.widget.ListView;
    import android.widget.TextView;
    import android.widget.Toast;
    
    import com.benhero.design.R;
    import com.benhero.design.mvp.bean.MvpItem;
    import com.benhero.design.mvp.presenter.MvpPresenterD;
    import com.benhero.design.mvp.presenter.MvpContract;
    import com.benhero.design.mvp.presenter.MvpPresenterB;
    import com.benhero.design.mvp.presenter.MvpPresenterC;
    import com.benhero.design.mvp.presenter.MvpPresenterA;
    
    import java.util.ArrayList;
    import java.util.List;
    
    
    /**
     * MVP
     *
     * @author benhero
     */
    public class MvpActivity extends AppCompatActivity implements MvpContract.View, View.OnClickListener {
        public static final String EXTRA_ENTER = "enter";
        /**
         * 1 : A
         */
        public static final int ENTER_A = 1;
        /**
         * 2 : B
         */
        public static final int ENTER_B = 2;
        /**
         * 3 : C
         */
        public static final int ENTER_C = 3;
        /**
         * 4 : D
         */
        public static final int ENTER_D = 4;
    
        private MvpContract.Presenter mPresenter;
        private TextView mUpgradeBtn;
        private ListView mListView;
        private List<MvpItem> mList = new ArrayList<>();
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_mvp_layout);
            initView();
            checkIntent();
            mListView.setAdapter(new MVPAdapter());
        }
    
        private void initView() {
            mUpgradeBtn = (TextView) findViewById(R.id.mvp_btn);
            mUpgradeBtn.setOnClickListener(this);
            mListView = (ListView) findViewById(R.id.mvp_listview);
        }
    
        private void checkIntent() {
            Intent intent = getIntent();
            if (intent != null) {
                int enter = intent.getIntExtra(EXTRA_ENTER, 0);
                if (enter == 0) {
                    errorEnter();
                } else {
                    initData(enter);
                }
            } else {
                errorEnter();
            }
        }
    
        /**
         * 状态错误
         */
        private void errorEnter() {
            Toast.makeText(this, "Error Intent", Toast.LENGTH_SHORT).show();
            finish();
        }
    
        private void initData(int extra) {
            switch (extra) {
                case ENTER_A:
                    mPresenter = new MvpPresenterA(this);
                    break;
                case ENTER_B:
                    mPresenter = new MvpPresenterB(this);
                    break;
                case ENTER_C:
                    mPresenter = new MvpPresenterC(this);
                    break;
                case ENTER_D:
                    mPresenter = new MvpPresenterD(this);
                    break;
                default:
                    errorEnter();
                    break;
            }
            if (mPresenter != null) {
                mPresenter.initData();
            }
        }
    
        @Override
        protected void onResume() {
            super.onResume();
            if (mPresenter != null) {
                mPresenter.onResume();
            }
        }
    
        @Override
        public void onClick(View v) {
            if (v.equals(mUpgradeBtn)) {
                Intent intent = new Intent(this, MvpResultActivity.class);
                intent.putExtra(MvpResultActivity.EXTRA_ENTER,
                        mPresenter != null ? mPresenter.getEnter() : MvpResultActivity.ENTER_MAIN);
                this.startActivity(intent);
            }
        }
    
        @Override
        public void initData(List<MvpItem> list) {
            mList.clear();
            mList.addAll(list);
        }
    
        @Override
        public void setTitleText(int id) {
            setTitle(getString(id));
        }
    
        @Override
        public void setUpgradeBtnText(int id) {
            mUpgradeBtn.setText(id);
        }
    
        @Override
        public void setPresenter(MvpContract.Presenter presenter) {
            mPresenter = presenter;
        }
    
        /**
         * 列表适配器
         *
         * @author benhero
         */
        private class MVPAdapter extends BaseAdapter {
    
            @Override
            public int getCount() {
                return mList.size();
            }
    
            @Override
            public Object getItem(int position) {
                return mList.get(position);
            }
    
            @Override
            public long getItemId(int position) {
                return position;
            }
    
            @Override
            public View getView(int position, View convertView, ViewGroup parent) {
                MyViewHolder holder;
                if (convertView == null) {
                    holder = new MyViewHolder();
                    convertView = LayoutInflater.from(MvpActivity.this).inflate(R.layout.mvp_list_item, parent, false);
                    holder.mIndex = (TextView) convertView.findViewById(R.id.mvp_index);
                    holder.mTitle = (TextView) convertView.findViewById(R.id.mvp_title);
                    holder.mDesc = (TextView) convertView.findViewById(R.id.mvp_desc);
                    holder.mDivider = convertView.findViewById(R.id.mvp_divider);
                    convertView.setTag(holder);
                } else {
                    holder = (MyViewHolder) convertView.getTag();
                }
                MvpItem itemBean = mList.get(position);
                holder.mIndex.setText(position + 1 + "");
                holder.mTitle.setText(itemBean.getTitleId());
                holder.mDesc.setText(itemBean.getDescId());
                holder.mDivider.setVisibility(position == mList.size() - 1 ? View.GONE : View.VISIBLE);
                return convertView;
            }
    
            /**
             * ViewHolder
             */
            class MyViewHolder {
                TextView mIndex;
                TextView mTitle;
                TextView mDesc;
                View mDivider;
            }
        }
    }
    
    

    以上就是我们对V层的处理,根据不同的intent数据,选择不同的MvpPresenter来处理不同的界面数据和交互逻辑。

    ②. P层

    以下是其中某个P层的代码案例。

    package com.benhero.design.mvp.presenter;
    
    
    import com.benhero.design.R;
    import com.benhero.design.mvp.bean.MvpItem;
    import com.benhero.design.mvp.view.MvpResultActivity;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * MvpPresenterA
     *
     * @author benhero
     */
    public class MvpPresenterA implements MvpContract.Presenter {
        private final MvpContract.View mView;
    
        public MvpPresenterA(MvpContract.View view) {
            mView = view;
        }
    
        @Override
        public void start() {
    
        }
    
        @Override
        public void initData() {
            List<MvpItem> list = new ArrayList<>();
            list.add(createFactor(R.string.mvp_a_factor_title_1, R.string.mvp_a_factor_desc_1));
            list.add(createFactor(R.string.mvp_a_factor_title_2, R.string.mvp_a_factor_desc_2));
            mView.initData(list);
            mView.setTitleText(R.string.mvp_a_title);
            mView.setUpgradeBtnText(R.string.mvp_a_upgrade_btn);
        }
    
        private MvpItem createFactor(int titleId, int descId) {
            MvpItem item = new MvpItem();
            item.setTitleId(titleId);
            item.setDescId(descId);
            return item;
        }
    
        @Override
        public int getEnter() {
            return MvpResultActivity.ENTER_A;
        }
    
        @Override
        public void onResume() {
    
        }
    }
    

    ③. V层与P层接口

    而对于V与P的接口类,是参考谷歌MVP架构开源项目中对于这方面的设计。
    具体到本文的案例,接口类如下:

    package com.benhero.design.mvp.presenter;
    
    
    import com.benhero.design.mvp.base.BasePresenter;
    import com.benhero.design.mvp.base.BaseView;
    import com.benhero.design.mvp.bean.MvpItem;
    
    import java.util.List;
    
    /**
     * MVP接口
     *
     * @author benhero
     */
    public interface MvpContract {
        /**
         * MVP逻辑控制接口
         */
        interface Presenter extends BasePresenter {
    
            void initData();
    
            int getEnter();
    
            void onResume();
        }
    
        /**
         * MVP界面接口
         */
        interface View extends BaseView<Presenter> {
    
            void initData(List<MvpItem> list);
    
            void setTitleText(int id);
    
            void setUpgradeBtnText(int id);
        }
    }
    

    4. 弊端

    MVP最大的弊端,应该是可读性。
    当M层和V层之间的互相调用过多时,在调试或者阅读代码时候,需要不停地在两边不停地跳转。而若不采用MVP,且代码排序良好,则可以自上而下顺畅地阅读。
    而影响可读性的另一个重大因素是接口!
    当你在阅读V层时,遇到一个P的调用,点击跳转,则先跳转到接口类,再点击跳转到实现,实在繁琐(当然也可以通过快捷键直接跳实现的方法)。

    5. 建议

    若M层或者V层不存在复用的可能性,则直接抛弃接口!
    接口本身是规范类的行为,从而实现复用,多态。
    对于某些业务的开发,根本不存在复用的可能性,可以大胆地抛弃之。
    接口还有另一个作用就是约束访问者的访问范围,视情况再决定是否使用。
    而对于复用的场景,接口肯定是必不可少的。


    二. 模块化

    我们开发过程中,经常存在这样的场景:Activity界面可以分成多个模块,且每个模块之间的交互不多。此时,我们就可以采用模块化的思路去解决Activity代码量过大的问题。

    1. 思路

    其实在实现这方面的需求,Google已经提供了解决方案:Fragment。一个Activity分切成多个Fragment,而且还可以针对不同屏幕来组合视图结构,相当好用。Fragment本身会处理好Activity相关的生命周期,非常棒。

    注意:若一个Activity里只包裹着一个Fragment,并且没有别的视图,那么没什么意义!年少时做过不少这种傻事了我。这种场景不如直接一个Activity。

    2. 新概念

    这里,需要引入一个新的概念:ViewHolder(你也可以用Presenter或者Module等来命名它)。
    作用:界面相关的业务逻辑的封装处理,轻量级。大概基础类如下,可以根据自己的需求进行调整。

    package com.benhero.design.module.base;
    
    import android.view.View;
    
    /**
     * ViewHolder基类
     *
     * @author benhero
     */
    public class ViewHolder {
        private View mContentView;
    
        public ViewHolder() {
        }
    
        public ViewHolder(View contentView) {
            mContentView = contentView;
        }
    
        public final void setContentView(View contentView) {
            mContentView = contentView;
        }
    
        public View getContentView() {
            return mContentView;
        }
    }
    

    3. 案例

    代码目录层次

    image.png

    以下是一个视图模块化比较清晰的界面,图如下,图1抽屉上滑后变成图2的效果,使用的是BottomSheet组件。

    image.png image.png

    接下来,将从Activity→Fragment→ViewHolder一层一层展示如何将相对复杂的Activity模块化。

    1. Activity

    package com.benhero.design.module.activity;
    
    import android.os.Bundle;
    import android.support.design.widget.BottomSheetBehavior;
    import android.support.v7.app.AppCompatActivity;
    import android.view.View;
    
    import com.benhero.design.R;
    
    /**
     * 模块化Activity
     *
     * @author benhero
     */
    public class ModuleActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_module);
            BottomSheetBehavior<View> behavior = BottomSheetBehavior.from(findViewById(R.id.activity_main_bottom_sheet));
            behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
        }
    }
    
    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout
        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:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#282828"
        tools:context="com.benhero.design.module.activity.ModuleActivity">
    
        <fragment
            android:id="@+id/activity_module_bg_fragment"
            android:name="com.benhero.design.module.bg.ModuleBgFragment"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:layout="@layout/fragment_module_bg"/>
    
        <android.support.design.widget.CoordinatorLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:clipChildren="false"
            android:clipToPadding="false">
    
            <RelativeLayout
                android:id="@+id/activity_main_bottom_sheet"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginLeft="@dimen/common_margin"
                android:layout_marginRight="@dimen/common_margin"
                android:clipChildren="false"
                android:clipToPadding="false"
                app:behavior_hideable="true"
                app:behavior_peekHeight="@dimen/main_bottom_sheet_peek_height"
                app:elevation="40dp"
                app:layout_behavior="android.support.design.widget.BottomSheetBehavior">
    
                <fragment
                    android:id="@+id/activity_main_bottom_sheet_fragment"
                    android:name="com.benhero.design.module.bottom.ModuleBottomFragment"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    tools:layout="@layout/fragment_module_bottom"/>
    
            </RelativeLayout>
    
        </android.support.design.widget.CoordinatorLayout>
    
    </RelativeLayout>
    

    2. 底层

    底层视图相对简单点,就是一个TextView,故没有继续拆分。

    package com.benhero.design.module.bg;
    
    
    import android.os.Bundle;
    import android.support.v4.app.Fragment;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.ViewGroup;
    
    import com.benhero.design.R;
    
    /**
     * 模块化背景Fragment
     */
    public class ModuleBgFragment extends Fragment {
    
        public ModuleBgFragment() {
    
        }
    
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                                 Bundle savedInstanceState) {
            return inflater.inflate(R.layout.fragment_module_bg, container, false);
        }
    }
    

    3. 抽屉

    ①. Fragment
    package com.benhero.design.module.bottom;
    
    
    import android.os.Bundle;
    import android.support.v4.app.Fragment;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.ViewGroup;
    
    import com.benhero.design.R;
    
    /**
     * 模块化抽屉Fragment
     */
    public class ModuleBottomFragment extends Fragment {
    
        private ModuleBottomPeekViewHolder mPeekViewHolder;
        private ModuleBottomListViewHolder mListViewHolder;
    
        public ModuleBottomFragment() {
    
        }
    
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                                 Bundle savedInstanceState) {
            View layout = inflater.inflate(R.layout.fragment_module_bottom, container, false);
            mPeekViewHolder = new ModuleBottomPeekViewHolder(this.getActivity(), layout.findViewById(R.id.bottom_peek_layout));
            mListViewHolder = new ModuleBottomListViewHolder(layout.findViewById(R.id.fragment_bottom_sheet_list));
            return layout;
        }
    }
    

    这里我们通过ViewHolder将抽屉分成了2个模块。另外,布局xml如下。

    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#6CD1CC"
        android:orientation="vertical"
        tools:context="com.benhero.design.module.bottom.ModuleBottomFragment">
    
        <LinearLayout
            android:id="@+id/bottom_peek_layout"
            android:layout_width="match_parent"
            android:layout_height="@dimen/main_bottom_sheet_peek_height"
            android:orientation="horizontal">
    
            <Button
                android:id="@+id/bottom_peek_btn_1"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:text="Btn 1"/>
    
            <Button
                android:id="@+id/bottom_peek_btn_2"
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1"
                android:text="Btn 2"/>
        </LinearLayout>
    
        <include
            layout="@layout/fragment_module_bottom_list"/>
    
    </LinearLayout>
    
    
    ②. ViewHolder
    package com.benhero.design.module.bottom;
    
    import android.content.Context;
    import android.view.View;
    import android.widget.Toast;
    
    import com.benhero.design.R;
    import com.benhero.design.module.base.ViewHolder;
    
    /**
     * 抽屉顶部的ViewHolder
     *
     * @author benhero
     */
    public class ModuleBottomPeekViewHolder extends ViewHolder implements View.OnClickListener {
    
        private final Context mContext;
        private View mBtn1;
        private View mBtn2;
    
        public ModuleBottomPeekViewHolder(Context context, View contentView) {
            super(contentView);
            mContext = context;
            initView();
        }
    
        private void initView() {
            View contentView = getContentView();
            mBtn1 = contentView.findViewById(R.id.bottom_peek_btn_1);
            mBtn2 = contentView.findViewById(R.id.bottom_peek_btn_2);
            mBtn1.setOnClickListener(this);
            mBtn2.setOnClickListener(this);
        }
    
    
        @Override
        public void onClick(View view) {
            if (view.equals(mBtn1)) {
                Toast.makeText(mContext, "Click Btn1", Toast.LENGTH_SHORT).show();
            } else if (view.equals(mBtn2)) {
                Toast.makeText(mContext, "Click Btn2", Toast.LENGTH_SHORT).show();
            }
        }
    }
    

    Github

    本文案例已上传至Github - DesignExplore


    总结

    对于以上两种模式的使用场景,大概如下。

    • 界面视图不可切割模块化,且视图、逻辑都不存在复用的可能:使用MVP,且无需抽接口
    • 界面视图或逻辑存在复用的情况:使用MVP,并抽接口
    • 界面视图可模块化,模块间较少关联:使用视图模块化的方式:Activity→Fragment→ViewHolder
    • 若界面非常复杂,可以考虑两种方式同时使用

    对于模块化的方案,不同模块间的通讯可以采用接口让上层去中转。更简单的是使用EventBus。


    对于程序设计,每个人的理解可能都不一样,但我们的目标都是一致的,都是想让程序的可读性逻辑性拓展性等各方面都达到比较好的效果。

    若你有好的想法,欢迎交流。

    相关文章

      网友评论

      • 乐意乐读::+1: 本外汉表示,不知所云
        乐意乐读:@Benhero 赞赞赞
        Benhero:哈,专业的,不需要懂,点赞就好:smile:
      • 53d0f98260b6:用Activity或者Fragment做P层要好一点,博主可以试试
        Benhero:@buaa追风的叶子 这个思路很棒,我理解这个是ListView的ViewHolder模式衍生出来的。既可以解耦,也可以分层,也可以省去接口,用在那种界面架构比较耦合的情况下,是非常不错的选择。
        53d0f98260b6:@奔ben苯笨 很早之前看的了,找不到原文章在哪里了,只找到这个:https://github.com/hehonghui/android-tech-frontier/tree/master/androidweekly/%E4%B8%80%E7%A7%8D%E5%9C%A8android%E4%B8%AD%E5%AE%9E%E7%8E%B0MVP%E6%A8%A1%E5%BC%8F%E7%9A%84%E6%96%B0%E6%80%9D%E8%B7%AF
        Benhero:那V层的封装是怎么一个处理方式?有案例可以了解下吗?

      本文标题:Android程序设计探索:MVP与模块化

      本文链接:https://www.haomeiwen.com/subject/kguoqxtx.html