[技術分享] Android程序設計探索:MVP與模塊化

 作者:新投云  發布于:2017-04-05  瀏覽數:

一. 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();
        }
    }
}

 

總結

對于以上兩種模式的使用場景,大概如下。

  • 界面視圖不可切割模塊化,且視圖、邏輯都不存在復用的可能:使用MVP,且無需抽接口
  • 界面視圖或邏輯存在復用的情況:使用MVP,并抽接口
  • 界面視圖可模塊化,模塊間較少關聯:使用視圖模塊化的方式:Activity→Fragment→ViewHolder
  • 若界面非常復雜,可以考慮兩種方式同時使用

對于模塊化的方案,不同模塊間的通訊可以采用接口讓上層去中轉。更簡單的是使用EventBus。


對于程序設計,每個人的理解可能都不一樣,但我們的目標都是一致的,都是想讓程序的可讀性邏輯性拓展性等各方面都達到比較好的效果。

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

相關文章

三期必中一期平特肖