android MVP模式思考

    在软件开发设计中,有多种软件设计模式,如web开发中经典的MVC, 将后台分为三层:Model层,View层和Controller层,其中,Model主要是数据处理,如数据库,文件,或网络数据等;View层是视图层,主要是指前端或后端用于直接展现给用户的页面,Controller层则是负责业务逻辑处理,即将Model中读取的数据显示到View层,同时又将View层触发的操作事件经过业务处理后将数据保存到Model层中。

    在android中,可能很多开发者使用的还是mvc模式,比如,在代码中可以发现大量的IxxEntity,IxxDao,IxxDaoImpl,IxxController,IxxControllerImpldengdeng。但是,Android开发中,还有一种不错的开发设计,那就是MVP模式。在Android4.4源码中的InCallUI中,我们会发现就应用了这种模式。

    首先,先解释下,什么是MVP,MVP模式其实跟MVC模式的意图是一样的,都是为了将视图,数据和业务层分离开了,从而更好的应用于后续的开发,升级以及维护。其中MVP中的P,代表Presenter,即主持的意思,主要负责业务逻辑处理,跟MVC模式不同的是,在MVP中View层是不允许跟Model层直接交互的。MVP模式的理想场景就是可以只修改View层的界面代码,但是Presenter层和model层不需要修改任何代码,这是因为在android中,开发者面对最多的需求就是界面,变化最多的也是界面,所以mvp模式对android开发来说是一个非常不错的选择,当然,弊端就是,额外增加的代码量也有点多。。。

    具体调用逻辑,借用下面网上一张图:

    接着,我们来看看android4.4中InCallUI源码中MVP代码的应用。

    InCallUI源码路径是在/packages/apps/InCallUI,我们直接看/src/com/android/incallui目录,会发现该目录下有一大堆xxxPresenter的类。其中,AnswerPresenter是处理接听电话的Presenter,CallButtonPresenter是处理拨号盘的Presenter,CallCardPresenter是通话界面的Presenter。在InCallUI中,从所有的界面中抽象出一个接口:Ui,它是一个空接口,它的子类分别对应不同的Ui,因为对应MVP每一个V,它的界面都是不同的,所以需要抽象出一个接口,并且让这个接口继承Ui,具体代码如下:

/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License
 */

package com.android.incallui;

/**
 * Base class for all presenter ui.
 */
public interface Ui {

}

    如前面所说,CallButtonXX是负责拨号盘,对于拨号盘的界面显示,InCallUI抽象了一个继承Ui的接口CallButtonUi(定义在CallButtonPresenter类中),相关代码如下:

    public interface CallButtonUi extends Ui {
        void setEnabled(boolean on);
        void setMute(boolean on);
        void enableMute(boolean enabled);
        void setHold(boolean on);
        void showHold(boolean show);
        void enableHold(boolean enabled);
        void showMerge(boolean show);
        void showSwap(boolean show);
        void showAddCall(boolean show);
        void enableAddCall(boolean enabled);
        void displayDialpad(boolean on);
        boolean isDialpadVisible();
        void setAudio(int mode);
        void setSupportedAudio(int mask);
        void showManageConferenceCallButton();
        void showGenericMergeButton();
        void hideExtraRow();
        void displayManageConferencePanel(boolean on);
    }

    在InCallUI中,抽象类Presenter是所有XXPresenter的父类,他主要实现了跟View交互时所有Presenter都所需的onUiReady和onUiUnready两个方法。具体代码如下:

/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License
 */

package com.android.incallui;

/**
 * Base class for Presenters.
 */
public abstract class Presenter<U extends Ui> {

    private U mUi;

    /**
     * Called after the UI view has been created.  That is when fragment.onViewCreated() is called.
     *
     * @param ui The Ui implementation that is now ready to be used.
     */
    public void onUiReady(U ui) {
        mUi = ui;
    }

    /**
     * Called when the UI view is destroyed in Fragment.onDestroyView().
     */
    public final void onUiDestroy(U ui) {
        onUiUnready(ui);
        mUi = null;
    }

    /**
     * To be overriden by Presenter implementations.  Called when the fragment is being
     * destroyed but before ui is set to null.
     */
    public void onUiUnready(U ui) {
    }

    public U getUi() {
        return mUi;
    }
}

其中Presenter中的实现CallButtonPresenter的代码如下:

/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License
 */

package com.android.incallui;

import com.android.incallui.AudioModeProvider.AudioModeListener;
import com.android.incallui.InCallPresenter.InCallState;
import com.android.incallui.InCallPresenter.InCallStateListener;
import com.android.incallui.InCallPresenter.IncomingCallListener;
import com.android.services.telephony.common.AudioMode;
import com.android.services.telephony.common.Call;
import com.android.services.telephony.common.Call.Capabilities;

import android.telephony.PhoneNumberUtils;

/**
 * Logic for call buttons.
 */
public class CallButtonPresenter extends Presenter<CallButtonPresenter.CallButtonUi>
        implements InCallStateListener, AudioModeListener, IncomingCallListener {

    //省略若干行代码

    public CallButtonPresenter() {
    }

    @Override
    public void onUiReady(CallButtonUi ui) {
        super.onUiReady(ui);

        AudioModeProvider.getInstance().addListener(this);

        // register for call state changes last
        InCallPresenter.getInstance().addListener(this);
        InCallPresenter.getInstance().addIncomingCallListener(this);
    }

    @Override
    public void onUiUnready(CallButtonUi ui) {
        super.onUiUnready(ui);

        InCallPresenter.getInstance().removeListener(this);
        AudioModeProvider.getInstance().removeListener(this);
        InCallPresenter.getInstance().removeIncomingCallListener(this);
    }

    @Override
    public void onStateChange(InCallState state, CallList callList) {

        //省略若干行代码
    }

    @Override
    public void onIncomingCall(InCallState state, Call call) {
        onStateChange(state, CallList.getInstance());
    }

    @Override
    public void onAudioMode(int mode) {
        if (getUi() != null) {
            getUi().setAudio(mode);
        }
    }

    @Override
    public void onSupportedAudioMode(int mask) {
        if (getUi() != null) {
            getUi().setSupportedAudio(mask);
        }
    }

    @Override
    public void onMute(boolean muted) {
        if (getUi() != null) {
            getUi().setMute(muted);
        }
    }

    public int getAudioMode() {
        return AudioModeProvider.getInstance().getAudioMode();
    }

    public int getSupportedAudio() {
        return AudioModeProvider.getInstance().getSupportedModes();
    }

    public void setAudioMode(int mode) {

        //省略若干行代码
    }

    /**
     * Function assumes that bluetooth is not supported.
     */
    public void toggleSpeakerphone() {
        //省略若干行代码
    }

    public void endCallClicked() {
        if (mCall == null) {
            return;
        }

        CallCommandClient.getInstance().disconnectCall(mCall.getCallId());
    }

    public void manageConferenceButtonClicked() {
        getUi().displayManageConferencePanel(true);
    }

    public void muteClicked(boolean checked) {
        Log.d(this, "turning on mute: " + checked);

        CallCommandClient.getInstance().mute(checked);
    }

    public void holdClicked(boolean checked) {
        if (mCall == null) {
            return;
        }

        Log.d(this, "holding: " + mCall.getCallId());

        CallCommandClient.getInstance().hold(mCall.getCallId(), checked);
    }

    public void mergeClicked() {
        CallCommandClient.getInstance().merge();
    }

    public void addCallClicked() {
        //省略若干行代码
    }

    public void swapClicked() {
        //省略若干行代码
    }

    public void showDialpadClicked(boolean checked) {
        //省略若干行代码
    }

    private void updateUi(InCallState state, Call call) {
        //省略若干行代码
    }

    private void updateExtraButtonRow() {
        //省略若干行代码
    }

    public void refreshMuteState() {
        //省略若干行代码
    }

    public interface CallButtonUi extends Ui {
        void setEnabled(boolean on);
        void setMute(boolean on);
        void enableMute(boolean enabled);
        void setHold(boolean on);
        void showHold(boolean show);
        void enableHold(boolean enabled);
        void showMerge(boolean show);
        void showSwap(boolean show);
        void showAddCall(boolean show);
        void enableAddCall(boolean enabled);
        void displayDialpad(boolean on);
        boolean isDialpadVisible();
        void setAudio(int mode);
        void setSupportedAudio(int mask);
        void showManageConferenceCallButton();
        void showGenericMergeButton();
        void hideExtraRow();
        void displayManageConferencePanel(boolean on);
    }
}

    在InCallUI中,除了InCallActivity,其他界面都是由Fragment负责界面,即,每个Fragment都属于MVP中的View,InCallUI中抽象出了一个BaseFragment来作为所有Fragment的父类,BaseFragment代码如下:

/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License
 */

package com.android.incallui;

import android.app.Fragment;
import android.os.Bundle;

/**
 * Parent for all fragments that use Presenters and Ui design.
 */
public abstract class BaseFragment<T extends Presenter<U>, U extends Ui> extends Fragment {

    private T mPresenter;

    abstract T createPresenter();

    abstract U getUi();

    protected BaseFragment() {
        mPresenter = createPresenter();
    }

    /**
     * Presenter will be available after onActivityCreated().
     *
     * @return The presenter associated with this fragment.
     */
    public T getPresenter() {
        return mPresenter;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        mPresenter.onUiReady(getUi());
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        mPresenter.onUiDestroy(getUi());
    }
}

    即在BaseFragment或继承自BaseFragment的子Fragment被创建的时候,将V(Fragment)跟P(Presenter)关联,在Fragment被销毁的时候,将V和P解绑。这里我们继续看看CallButtonFragment的代码:

/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License
 */

package com.android.incallui;

import android.graphics.drawable.LayerDrawable;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.CompoundButton;
import android.widget.ImageButton;
import android.widget.PopupMenu;
import android.widget.PopupMenu.OnDismissListener;
import android.widget.PopupMenu.OnMenuItemClickListener;
import android.widget.ToggleButton;

import com.android.services.telephony.common.AudioMode;

/**
 * Fragment for call control buttons
 */
public class CallButtonFragment
        extends BaseFragment<CallButtonPresenter, CallButtonPresenter.CallButtonUi>
        implements CallButtonPresenter.CallButtonUi, OnMenuItemClickListener, OnDismissListener,
        View.OnClickListener, CompoundButton.OnCheckedChangeListener {

    private ImageButton mMuteButton;
    private ImageButton mAudioButton;
    private ImageButton mHoldButton;
    private ToggleButton mShowDialpadButton;
    private ImageButton mMergeButton;
    private ImageButton mAddCallButton;
    private ImageButton mSwapButton;

    private PopupMenu mAudioModePopup;
    private boolean mAudioModePopupVisible;
    private View mEndCallButton;
    private View mExtraRowButton;
    private View mManageConferenceButton;
    private View mGenericMergeButton;

    @Override
    CallButtonPresenter createPresenter() {
        // TODO: find a cleaner way to include audio mode provider than
        // having a singleton instance.
        return new CallButtonPresenter();
    }

    @Override
    CallButtonPresenter.CallButtonUi getUi() {
        return this;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        //省略若干行代码

        mManageConferenceButton = parent.findViewById(R.id.manageConferenceButton);
        mManageConferenceButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                getPresenter().manageConferenceButtonClicked();
            }
        });
        mGenericMergeButton = parent.findViewById(R.id.cdmaMergeButton);
        mGenericMergeButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                getPresenter().mergeClicked();
            }
        });
        //省略若干行代码
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        //省略若干行代码
    }

    @Override
    public void onResume() {
        if (getPresenter() != null) {
            getPresenter().refreshMuteState();
        }
        //省略若干行代码
    }

    @Override
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
    }

    @Override
    public void onClick(View view) {
        //省略若干行代码
    }

    @Override
    public void setEnabled(boolean isEnabled) {
       //省略若干行代码
    }

    @Override
    public void setMute(boolean value) {
        mMuteButton.setSelected(value);
    }

    @Override
    public void enableMute(boolean enabled) {
        mMuteButton.setEnabled(enabled);
    }

    @Override
    public void setHold(boolean value) {
        mHoldButton.setSelected(value);
    }

    @Override
    public void showHold(boolean show) {
        mHoldButton.setVisibility(show ? View.VISIBLE : View.GONE);
    }

    @Override
    public void enableHold(boolean enabled) {
        mHoldButton.setEnabled(enabled);
    }

    @Override
    public void showMerge(boolean show) {
        mMergeButton.setVisibility(show ? View.VISIBLE : View.GONE);
    }

    @Override
    public void showSwap(boolean show) {
        mSwapButton.setVisibility(show ? View.VISIBLE : View.GONE);
    }

    @Override
    public void showAddCall(boolean show) {
        mAddCallButton.setVisibility(show ? View.VISIBLE : View.GONE);
    }

    @Override
    public void enableAddCall(boolean enabled) {
        mAddCallButton.setEnabled(enabled);
    }

    @Override
    public void setAudio(int mode) {
        updateAudioButtons(getPresenter().getSupportedAudio());
        refreshAudioModePopup();
    }

    @Override
    public void setSupportedAudio(int modeMask) {
        updateAudioButtons(modeMask);
        refreshAudioModePopup();
    }

    @Override
    public boolean onMenuItemClick(MenuItem item) {
        //省略若干行代码
    }

    // PopupMenu.OnDismissListener implementation; see showAudioModePopup().
    // This gets called when the PopupMenu gets dismissed for *any* reason, like
    // the user tapping outside its bounds, or pressing Back, or selecting one
    // of the menu items.
    @Override
    public void onDismiss(PopupMenu menu) {
        Log.d(this, "- onDismiss: " + menu);
        mAudioModePopupVisible = false;
    }

    /**
     * Checks for supporting modes.  If bluetooth is supported, it uses the audio
     * pop up menu.  Otherwise, it toggles the speakerphone.
     */
    private void onAudioButtonClicked() {
        Log.d(this, "onAudioButtonClicked: " +
                AudioMode.toString(getPresenter().getSupportedAudio()));

        if (isSupported(AudioMode.BLUETOOTH)) {
            showAudioModePopup();
        } else {
            getPresenter().toggleSpeakerphone();
        }
    }

    /**
     * Refreshes the "Audio mode" popup if it's visible.  This is useful
     * (for example) when a wired headset is plugged or unplugged,
     * since we need to switch back and forth between the "earpiece"
     * and "wired headset" items.
     *
     * This is safe to call even if the popup is already dismissed, or even if
     * you never called showAudioModePopup() in the first place.
     */
    public void refreshAudioModePopup() {
        if (mAudioModePopup != null && mAudioModePopupVisible) {
            // Dismiss the previous one
            mAudioModePopup.dismiss();  // safe even if already dismissed
            // And bring up a fresh PopupMenu
            showAudioModePopup();
        }
    }

    /**
     * Updates the audio button so that the appriopriate visual layers
     * are visible based on the supported audio formats.
     */
    private void updateAudioButtons(int supportedModes) {
        //省略若干行代码
    }

    private boolean isSupported(int mode) {
        return (mode == (getPresenter().getSupportedAudio() & mode));
    }

    private boolean isAudio(int mode) {
        return (mode == getPresenter().getAudioMode());
    }

    @Override
    public void displayDialpad(boolean value) {
        mShowDialpadButton.setChecked(value);
        if (getActivity() != null && getActivity() instanceof InCallActivity) {
            ((InCallActivity) getActivity()).displayDialpad(value);
        }
    }

    @Override
    public boolean isDialpadVisible() {
        if (getActivity() != null && getActivity() instanceof InCallActivity) {
            return ((InCallActivity) getActivity()).isDialpadVisible();
        }
        return false;
    }

    @Override
    public void displayManageConferencePanel(boolean value) {
        if (getActivity() != null && getActivity() instanceof InCallActivity) {
            ((InCallActivity) getActivity()).displayManageConferencePanel(value);
        }
    }


    @Override
    public void showManageConferenceCallButton() {
        mExtraRowButton.setVisibility(View.VISIBLE);
        mManageConferenceButton.setVisibility(View.VISIBLE);
        mGenericMergeButton.setVisibility(View.GONE);
    }

    @Override
    public void showGenericMergeButton() {
        mExtraRowButton.setVisibility(View.VISIBLE);
        mManageConferenceButton.setVisibility(View.GONE);
        mGenericMergeButton.setVisibility(View.VISIBLE);
    }

    @Override
    public void hideExtraRow() {
       mExtraRowButton.setVisibility(View.GONE);
    }
}

 从而,通过Ui,CallButtonUi,BaseFragment,CallButtonFragment,Presenter,CallButtonPresenter将M,V,P分开,让数据,业务,展示分开开发维护,代码变得清晰,每层只需要关注自己的东西就行,这就比我们以前都只在一个Activity或Fragment中糅杂在一起好很多。

  如果有需要查看Android源码的童鞋,可以自行到Android官网下载或去下面两个网站进行在线查看。

 1,http://androidxref.com

 2,http://www.grepcode.com

posted on 2016-04-17 15:52  小样来玩玩  阅读(1143)  评论(0编辑  收藏  举报

导航