Android 你應(yīng)該學(xué)會的設(shè)計模式MVP

MVP大家最先想到的應(yīng)該是LOL和CF里面的MVP榮譽(yù)吧,玩過的應(yīng)該都知道指的是Most-Valuable-Player(全場表現(xiàn)最佳DE游戲玩家)

以前也經(jīng)常玩,平常凌晨一兩點(diǎn),周末凌晨三四點(diǎn)描验,想想那段時間還是挺瘋的占哟,浪費(fèi)了那么多時間,現(xiàn)在得趕緊抓緊時間好好學(xué)習(xí)翩迈,多寫簡書持灰,好好工作,定個小目標(biāo)负饲,掙他一個億堤魁!

好了廢話就不多說,進(jìn)入正題返十,我們這里說的MVP則是一種設(shè)計模式妥泉,說起MVP那就要先說說MVC設(shè)計模式了,MVP就是由MVC演變而來的洞坑。

MVC由Model盲链、View、Control組成迟杂。
Model數(shù)據(jù)模型刽沾,提供數(shù)據(jù)
View視圖模型,提供視圖展示
Control控制器排拷,負(fù)責(zé)控制Model和View通信

MVC在Android中的應(yīng)用悠轩,如下圖,Activity為Control攻泼,XML為View火架,請求網(wǎng)絡(luò)數(shù)據(jù)模塊為Model。View箭頭指向Control意為傳遞數(shù)據(jù)忙菠,那就是Control獲取View的數(shù)據(jù)傳遞給Model來請求網(wǎng)絡(luò)何鸡,請求到的數(shù)據(jù)直接傳遞給View來顯示,這是一條主線牛欢。還有一條就是底部這兩個箭頭骡男,也就是說Model可以不通過Control,直接獲取View的數(shù)據(jù)傍睹,然后再返回結(jié)果數(shù)據(jù)給View隔盛。

MVC.png

MVP由Model、View拾稳、Presenter組成吮炕。
Model數(shù)據(jù)模型,提供數(shù)據(jù)
View視圖模型访得,提供視圖展示
Presenter主持者龙亲,負(fù)責(zé)邏輯處理

MVP與MVC最明顯的區(qū)別就在于Presenter和Control了陕凹。如下圖,Presenter傳遞數(shù)據(jù)給Model鳄炉,Model得到數(shù)據(jù)來請求網(wǎng)絡(luò)杜耙,再返回數(shù)據(jù)給Presenter,Presenter再把得到的Model數(shù)據(jù)傳遞給View顯示拂盯。MVP整體的一個流程就是這樣佑女。相比MVC來說,Model和View互相不干涉谈竿,達(dá)到完全解耦

MVP.png

估計上面文字性的解釋都聽得有點(diǎn)暈珊豹,那我們就拿個例子來實(shí)戰(zhàn)一下吧

下面和大家一起完成一個簡單查詢快遞信息的例子,里面用到了當(dāng)下最流行的架構(gòu)RxJava+Retrofit+MVP+OkHttp榕订,非常值得大家一學(xué)

配置環(huán)境

在builde.gradle里面添加

compile 'com.android.support:appcompat-v7:23.3.0'
compile 'com.squareup.retrofit2:retrofit:2.1.0'
compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0'
compile 'com.squareup.retrofit2:converter-gson:2.1.0'
compile 'com.squareup.okhttp3:okhttp:3.4.1'
compile 'io.reactivex:rxandroid:1.2.1'
compile 'io.reactivex:rxjava:1.1.6'
compile 'com.squareup.okhttp3:logging-interceptor:3.4.1'

在AndroidManifest.xml添加所需權(quán)限

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

初始化配置Retrofit

public class MainApp extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        //初始化retrofitUtils
        RetrofitUtils.getInstance().initOkHttp(this);
    }
}

在MainApp中初始化Retrofit,設(shè)置超時和baseUrl,添加了日志攔截以及RxJava和數(shù)據(jù)解析蜕便,并暴露一個getRetrofit()方法供需要的地方調(diào)用

public class RetrofitUtils {
    private Retrofit retrofit;
    private String baseUrl = "http://www.kuaidi100.com/";
    private static class SingleLoader{
        private static final RetrofitUtils INSTANCE = new RetrofitUtils();
    }
    public static RetrofitUtils getInstance(){
        return SingleLoader.INSTANCE;
    }
    public void initOkHttp(@NonNull Context context){
        HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
        interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
        OkHttpClient client = new OkHttpClient.Builder()
                .connectTimeout(10000L,TimeUnit.MILLISECONDS)       //設(shè)置連接超時
                .readTimeout(10000L,TimeUnit.MILLISECONDS)          //設(shè)置讀取超時
                .writeTimeout(10000L, TimeUnit.MILLISECONDS)         //設(shè)置寫入超時
                .cache(new Cache(context.getCacheDir(),10 * 1024 * 1024))   //設(shè)置緩存目錄和10M緩存
                .addInterceptor(interceptor)    //添加日志攔截器(該方法也可以設(shè)置公共參數(shù)劫恒,頭信息)
                .build();
        retrofit = new Retrofit.Builder()
                .client(client)     //設(shè)置OkHttp
                .baseUrl(baseUrl)
                .addConverterFactory(GsonConverterFactory.create()) //  添加數(shù)據(jù)解析ConverterFactory
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())   //添加RxJava
                .build();
    }
    public Retrofit getRetrofit(){
        return retrofit;
    }
}

Model

public interface PostSearchModel {
    /**
     * 請求快遞信息
     * @param type 快遞類型
     * @param postid 快遞單號
     * @param callback 結(jié)果回調(diào)
     */
    void requestPostSearch(String type,String postid,PostSearchCallback callback);
    interface PostSearchCallback{
        void requestPostSearchSuccess(PostQueryInfo postQueryInfo);
        void requestPostSearchFail(String failStr);
    }
}

創(chuàng)建這個PostSearchModel接口,方便后期可以不修改之前代碼靈活切換多種實(shí)現(xiàn)方式

public interface PostServiceBiz {
    @POST("query")
    Observable<PostQueryInfo> searchRx(@Query("type") String type, @Query("postid") String postid);
}
public class PostQueryInfo {
    private String message;
    private String nu;
    private String ischeck;
    private String com;
    private String status;
    private String condition;
    private String state;
    private List<DataBean> data;
    public static class DataBean {
        private String time;
        private String context;
        private String ftime;
    }
}

PostQueryInfo沒有添加get和set方法轿腺,大家自行快捷鍵了
PostSearchModelImpl實(shí)現(xiàn)上面PostSearchModel接口两嘴,利用上一篇文章里學(xué)的Retrofit結(jié)合RxJava來請求網(wǎng)絡(luò)
這里傳了PostSearchCallback回調(diào)函數(shù),成功或者失敗都通過回調(diào)函數(shù)返回族壳,完全不需要考慮其它

public class PostSearchModelImpl implements PostSearchModel {
    @Override
    public void requestPostSearch(String type, String postid, final PostSearchCallback callback) {
        RetrofitUtils.getInstance()
                .getRetrofit()
                .create(PostServiceBiz.class)
                .searchRx(type,postid)
                //訪問網(wǎng)絡(luò)切換異步線程
                .subscribeOn(Schedulers.io())
                //響應(yīng)結(jié)果處理切換成主線程
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Subscriber<PostQueryInfo>() {
                    @Override
                    public void onCompleted() {
                        //請求結(jié)束回調(diào)
                    }
                    @Override
                    public void onError(Throwable e) {
                        //錯誤回調(diào)
                        callback.requestPostSearchFail(e.getMessage());
                    }
                    @Override
                    public void onNext(PostQueryInfo postQueryInfo) {
                        //成功結(jié)果返回
                        callback.requestPostSearchSuccess(postQueryInfo);
                    }
                });
    }
}

View

activity_main.xml主界面布局

<?xml version="1.0" encoding="utf-8"?>
<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:padding="16dp"
    tools:context=".ui.MainActivity"
    android:orientation="vertical">
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="快遞公司名:"/>
        <EditText
            android:id="@+id/post_name_et"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="yuantong"/>
    </LinearLayout>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="快 遞  單 號:"
            />
        <EditText
            android:id="@+id/post_id_et"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="500379523313"/>
    </LinearLayout>
    <Button
        android:id="@+id/post_search_bn"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="查詢"/>
    <ListView
        android:id="@+id/post_list_lv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:listSelector="@null"
        android:background="@null"/>
</LinearLayout>

view_item_logistics.xml
listview適配器的item布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#FFFFFF"
    android:orientation="vertical"
    android:padding="15dp">
    <TextView
        android:id="@+id/tv_conent"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="#8f8f8f"
        android:textSize="14sp"
        android:text="【深圳市】廣東分公司已出發(fā)" />
    <TextView
        android:layout_marginTop="13dp"
        android:id="@+id/tv_date"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="#8f8f8f"
        android:textSize="12sp"
        android:layout_marginBottom="13dp"
        android:paddingLeft="6dp"
        android:text="2016-05-03 18:23" />
</LinearLayout>

ListView適配器

public class LogisticsAdapter extends BaseAdapter {
    private List<PostQueryInfo.DataBean> datas;
    private LayoutInflater inflater;
    public LogisticsAdapter(Context context, List<PostQueryInfo.DataBean> datas) {
        this.datas = datas;
        inflater = LayoutInflater.from(context);
    }
    @Override
    public View getView(int i, View view, ViewGroup viewGroup) {
        PostQueryInfo.DataBean data = datas.get(i);
        view = inflater.inflate(R.layout.view_item_logistics, null);
        TextView tv_content= (TextView) view.findViewById(R.id.tv_conent);
        TextView tv_date= (TextView) view.findViewById(R.id.tv_date);
        tv_content.setText(data.getContext().replace("[","【").replace("]","】"));
        tv_date.setText(data.getTime());
        return view;
    }
    @Override
    public int getCount() {
        return datas != null ? datas.size() : 0;
    }
    @Override
    public Object getItem(int i) {
        return datas.get(i);
    }
    @Override
    public long getItemId(int i) {
        return i;
    }
}

BaseView提供通用顯示和隱藏加載框憔辫,如果有通用的方法都可以在寫在這里面

public interface BaseView {
    void showProgressDialog();    
    void hideProgressDialog();
}

MainView接口繼承提供MainActivity所有需要更新界面UI的方法

public interface MainView extends BaseView{
    void updateListUI(PostQueryInfo postQueryInfo);
    void errorToast(String message);
}

BaseActivity實(shí)現(xiàn)BaseVew所有接口方法

public class BaseActivity extends Activity implements BaseView{
    private ProgressDialog progressDialog;
    @Override
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        progressDialog = new ProgressDialog(this);
        progressDialog.setMessage("查詢中...");
    }
    @Override
    public void showProgressDialog(){
        if(progressDialog!=null){
            progressDialog.show();
        }
    }
    @Override
    public void hideProgressDialog(){
        if(progressDialog!=null&&progressDialog.isShowing()){
            progressDialog.dismiss(); 
       }
    }
}

MainActivity繼承BaseActivity實(shí)現(xiàn)MainView所有方法供PostPresenter調(diào)用,點(diǎn)擊查詢按鈕通過實(shí)例化的PostPresenter仿荆,調(diào)用相關(guān)方法贰您,后面的事情就全部交給PostPresenter去處理了,在activity銷毀的時候調(diào)用postPresenter.detach()防止內(nèi)存泄漏

public class MainActivity extends BaseActivity implements View.OnClickListener, MainView {
    private EditText post_name_et;
    private EditText post_id_et;
    private ListView post_list_lv;
    private Button post_search_bn;
    private PostPresenter postPresenter;
    private List<PostQueryInfo.DataBean> dataArray = new ArrayList<>();
    private LogisticsAdapter logisticsAdapter;
    @Override
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
        initData();
        initEvent();
    }
    private void initView() {
        post_name_et = (EditText) findViewById(R.id.post_name_et);
        post_id_et = (EditText) findViewById(R.id.post_id_et);
        post_list_lv = (ListView) findViewById(R.id.post_list_lv);
        post_search_bn = (Button) findViewById(R.id.post_search_bn);
    }
    private void initData() {
        logisticsAdapter = new LogisticsAdapter(getApplicationContext(),dataArray);
        postPresenter = new PostPresenter(this);
        post_list_lv.setAdapter(logisticsAdapter);
    }
    private void initEvent() {
        post_search_bn.setOnClickListener(this);
    }
    @Override
    public void onClick(View v) {
        switch(v.getId()){
            case R.id.post_search_bn:
           postPresenter.requestHomeData(post_name_et.getText().toString(),post_id_et.getText().toString());
                break;
        }
    }
    @Override
    public void updateListUI(PostQueryInfo postQueryInfo) {
        dataArray.clear();
        dataArray.addAll(postQueryInfo.getData());
        logisticsAdapter.notifyDataSetChanged();
    }
    @Override
    public void errorToast(String message) {
        Toast.makeText(getApplicationContext(),message,Toast.LENGTH_LONG).show();
    }
    @Override
    protected void onDestroy(){
        //防止activity銷毀,postPresenter對象還存在造成的內(nèi)存泄漏
        if(postPresenter!=null) postPresenter.detach();
        super.onDestroy();
    }
}

Presenter

BasePresenter基類拢操,提供了一個通用view對象锦亦,以及初始化和銷毀view對象的方法

public abstract class BasePresenter<T>{
    public T mView;
    public void attach(T view){
        this.mView = view;
    }
    public void detach(){
        mView = null;
    }
}

PostPresenter在構(gòu)造函數(shù)中通過基類attach的方法初始化MainActivity提供的view對象,實(shí)例化PostSearchModelImpl對象令境,通過它提供的方法來請求網(wǎng)絡(luò)數(shù)據(jù)杠园,在成功和失敗回調(diào)的函數(shù)里,可以做一些邏輯處理舔庶,通過view對象更新相應(yīng)的UI

public class PostPresenter extends BasePresenter<MainView>{
    private PostSearchModel postSearchModel;
    public PostPresenter(MainView mainView){
        attach(mainView);
        postSearchModel = new PostSearchModelImpl();
    }
    public void requestHomeData(String type,String postid){
        if(postSearchModel == null||mView == null)
            return;
        mView.showProgressDialog();
        postSearchModel.requestPostSearch(type, postid, new PostSearchModel.PostSearchCallback() {
            @Override
            public void requestPostSearchSuccess(PostQueryInfo postQueryInfo) {
                mView.hideProgressDialog();
                if(postQueryInfo!=null&&"ok".equals(postQueryInfo.getMessage())) {
                    mView.updateListUI(postQueryInfo);
                }
            }
            @Override
            public void requestPostSearchFail(String failStr) {
                mView.hideProgressDialog();
                mView.errorToast(failStr);
            }
        });
    }
}

到這里全部代碼基本都已編寫完抛蚁,代碼的結(jié)構(gòu)如下圖:

MVP結(jié)構(gòu)圖.png

下面就運(yùn)行程序看看效果吧,快遞公司名和快遞單號默認(rèn)填寫上去了惕橙,大家只需要點(diǎn)擊查詢瞧甩,結(jié)果和下面圖片一樣就說明成功了

快遞查詢.png

為什么沒有直接提供一個可以運(yùn)行的DEMO出來,原因很簡單就是希望大家能夠跟著一步一步學(xué)習(xí)弥鹦,然后動手慢慢邊敲代碼邊理解亲配,最后敲完運(yùn)行成功,應(yīng)該就會更加理解MVP了。

Tips

上面MVC吼虎、MVP流程圖制作的網(wǎng)站分享個大家:
https://www.processon.com/
這個網(wǎng)站還可以做四維導(dǎo)圖犬钢、UI原型圖和UML圖等等很多,有需要的可以去看看

成功源于不斷的學(xué)習(xí)和積累

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末思灰,一起剝皮案震驚了整個濱河市玷犹,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌洒疚,老刑警劉巖歹颓,帶你破解...
    沈念sama閱讀 222,590評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異油湖,居然都是意外死亡巍扛,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,157評論 3 399
  • 文/潘曉璐 我一進(jìn)店門乏德,熙熙樓的掌柜王于貴愁眉苦臉地迎上來撤奸,“玉大人,你說我怎么就攤上這事喊括‰使希” “怎么了?”我有些...
    開封第一講書人閱讀 169,301評論 0 362
  • 文/不壞的土叔 我叫張陵郑什,是天一觀的道長府喳。 經(jīng)常有香客問我,道長蘑拯,這世上最難降的妖魔是什么钝满? 我笑而不...
    開封第一講書人閱讀 60,078評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮申窘,結(jié)果婚禮上舱沧,老公的妹妹穿的比我還像新娘。我一直安慰自己偶洋,他們只是感情好熟吏,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,082評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著玄窝,像睡著了一般牵寺。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上恩脂,一...
    開封第一講書人閱讀 52,682評論 1 312
  • 那天帽氓,我揣著相機(jī)與錄音,去河邊找鬼俩块。 笑死黎休,一個胖子當(dāng)著我的面吹牛浓领,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播势腮,決...
    沈念sama閱讀 41,155評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼联贩,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了捎拯?” 一聲冷哼從身側(cè)響起泪幌,我...
    開封第一講書人閱讀 40,098評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎署照,沒想到半個月后祸泪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,638評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡建芙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,701評論 3 342
  • 正文 我和宋清朗相戀三年没隘,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片禁荸。...
    茶點(diǎn)故事閱讀 40,852評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡右蒲,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出屡限,到底是詐尸還是另有隱情,我是刑警寧澤炕倘,帶...
    沈念sama閱讀 36,520評論 5 351
  • 正文 年R本政府宣布钧大,位于F島的核電站,受9級特大地震影響罩旋,放射性物質(zhì)發(fā)生泄漏啊央。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,181評論 3 335
  • 文/蒙蒙 一涨醋、第九天 我趴在偏房一處隱蔽的房頂上張望瓜饥。 院中可真熱鬧,春花似錦浴骂、人聲如沸乓土。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,674評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽趣苏。三九已至,卻和暖如春梯轻,著一層夾襖步出監(jiān)牢的瞬間食磕,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,788評論 1 274
  • 我被黑心中介騙來泰國打工喳挑, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留彬伦,地道東北人滔悉。 一個月前我還...
    沈念sama閱讀 49,279評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像单绑,于是被迫代替她去往敵國和親回官。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,851評論 2 361

推薦閱讀更多精彩內(nèi)容