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

一. 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。


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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值