搭建自己的 Android MVP 快速開發(fā)框架

Android 開發(fā)進(jìn)入「死丟丟」的時(shí)代后账嚎,引用三方庫(kù)在 Gradle 的支持下變得十分輕松今缚。各種高手寫的開源框架察皇,極大程度降低了新手入行(坑)的門檻蕉堰,「一周開發(fā)一款 App 并上線」也不再遙不可及。
關(guān)于快速開發(fā)搁骑,筆者本人的意見是不一定什么功能都自己寫斧吐,但框架最好是自己搭。雖然網(wǎng)上有很多非常成熟好用的完整框架仲器,但直接「拿來(lái)主義」的話可能有 2 點(diǎn)不妥之處——

  • 框架提供的功能你未必都用得到煤率。比如你只寫一個(gè)純閱讀類型的應(yīng)用(不帶大數(shù)據(jù)收藏功能),那么你就用不到本地?cái)?shù)據(jù)庫(kù)乏冀,這樣完整框架里有關(guān)數(shù)據(jù)庫(kù)的內(nèi)容蝶糯,就給白白浪費(fèi)了。
  • 高手也有疏忽時(shí)辆沦,即便技術(shù)大牛昼捍,也不敢保證自己寫的代碼沒有任何 bug,在任何使用場(chǎng)景都健壯堅(jiān)挺肢扯。如果某天突然發(fā)現(xiàn)完整框架有什么 bug 或者局限妒茬,自己又沒能力解決,到頭來(lái)只能重構(gòu)大塊內(nèi)容甚至整個(gè)項(xiàng)目蔚晨,這代價(jià)就非常大了乍钻。

綜上,筆者更傾向新手「站在巨人的肩膀上搭積木」铭腕,用高手寫的不同功能庫(kù)银择,自己動(dòng)手搭屬于自己的快速開發(fā)框架。而且在搭框架的過程中累舷,你能不知不覺中學(xué)到很多進(jìn)階知識(shí)浩考,對(duì)自己的成長(zhǎng)也很有利。
限于水平和篇幅笋粟,筆者只用老牌輪子 Volley 做例子怀挠,搭一個(gè)僅涉及網(wǎng)絡(luò)請(qǐng)求和圖片加載的 MVP 框架析蝴。當(dāng)下最流行的原生框架應(yīng)屬 RxJava + Retrofit + OkHttp + Dagger 害捕,如果你想了解得更多,推薦下面幾篇文章——

當(dāng)然闷畸,這些庫(kù)本質(zhì)和 Volley 一樣尝盼,都是去實(shí)現(xiàn)具體功能的輪子,而 MVP 的架構(gòu)是不變的佑菩,所以下文的內(nèi)容對(duì)它們同樣適用盾沫。

動(dòng)手開始

打開 Android Studio裁赠,新建一個(gè)項(xiàng)目 MvpFrameTest。再在項(xiàng)目根目錄右鍵 new 一個(gè) Module赴精,選擇第二項(xiàng) Android Library 佩捞,取名 MVP
這時(shí)你會(huì)看到你的項(xiàng)目下面多了一個(gè)叫 mvp 的 Module(和 app 一樣是加粗顯示的)蕾哟,不過角標(biāo)是一個(gè)書架而非手機(jī)一忱。這代表此模塊是一個(gè)依賴庫(kù),而非獨(dú)立運(yùn)行的應(yīng)用谭确,我們今天主要的代碼都寫在它里面帘营。

1.png

導(dǎo)入依賴

點(diǎn)開 mvp 下面的 build.gradle 文件(別錯(cuò)搞成 app 下面的了哦),在 dependency 節(jié)點(diǎn)下面導(dǎo)入我們要用的輪子——

compile 'com.android.support:design:25.3.1'
compile 'com.android.volley:volley:1.0.0'
compile 'com.google.code.gson:gson:2.7'

這里我希望內(nèi)容盡量簡(jiǎn)潔一點(diǎn)逐哈,因此只導(dǎo)入設(shè)計(jì)適配(包含 RecyclerView 以及各種 Material Design 控件)芬迄、Volley(包含網(wǎng)絡(luò)請(qǐng)求圖片加載)和 Gson(包含 Json 解析)三個(gè)庫(kù)。語(yǔ)句后面的版本號(hào)僅供參考昂秃,因?yàn)楫?dāng)你看到這篇文章時(shí)禀梳,建議使用的版本號(hào)可能又變了。
下面點(diǎn)開 app 下面的 build.gradle 文件肠骆,同樣在 dependency 節(jié)點(diǎn)下面添加依賴——

compile project(path: ':mvp')

點(diǎn)擊提示行里面的 Sync Now出皇,一會(huì)兒任務(wù)完成,從此以后你在 mvp 里面依賴的庫(kù)(包括自己寫的各種類)哗戈,app 就都可以用了郊艘。
建議把整體性的功能諸如網(wǎng)絡(luò)請(qǐng)求、圖片加載等寫在 mvp 里唯咬,具體性的實(shí)現(xiàn)諸如 UI 配色纱注、訪問地址寫在 app 里,這樣你的框架使用起來(lái)才更加靈活胆胰。
如果你對(duì) Gradle 還不了解狞贱,推薦一篇文——

MVP

下面開始寫自己的東西了,由于我們想要的是 MVP 設(shè)計(jì)模式蜀涨,首先應(yīng)該完成通用的 M瞎嬉、V 和 P。對(duì) MVP 不了解的推薦一篇文——

找到 mvp 下面的 com.example.mvp 包厚柳,如圖所示

2.png

在里面新建兩個(gè)接口(Interface)氧枣,分別取名 BaseViewBaseModel

public interface BaseView {
    void showLoading();
    void hideLoading();
    void showError();
}

BaseView 里面我們定義了三個(gè)抽象方法别垮,分別用于顯示加載便监、隱藏加載和顯示加載失敗的內(nèi)容。這些方法最終會(huì)交給你的視圖(也就是 Activity 或者 Fragment)去實(shí)現(xiàn)。

public interface BaseModel {
}

BaseModel 里面目前可以什么都不寫烧董。如果你參與一個(gè)團(tuán)隊(duì)開發(fā)毁靶,接口和數(shù)據(jù)有比較統(tǒng)一的格式,那可以在此做一些規(guī)范工作逊移。
OK预吆,M 和 V 都有了,再新建一個(gè)抽象類胳泉,取名 BasePresenter啡浊。

public abstract class BasePresenter<M, V> {
    protected M mModel;
    protected WeakReference<V> mViewRef;

    protected void onAttach(M model, V view) {
        mModel = model;
        mViewRef = new WeakReference<>(view);
    }

    protected V getView() {
        return isViewAttached() ? mViewRef.get() : null;
    }

    protected boolean isViewAttached() {
        return null != mViewRef && null != mViewRef.get();
    }

    protected void onDetach() {
        if (null != mViewRef) {
            mViewRef.clear();
            mViewRef = null;
        }
    }
}

首先聲明了兩個(gè)泛型 M 和 V,M 對(duì)應(yīng)要處理的 Model胶背,V 則對(duì)應(yīng)負(fù)責(zé)展示的View巷嚣。由于 V 一般比較大,這里采用了弱引用的寫法钳吟,避免內(nèi)存泄漏廷粒。
isViewAttached() 用于檢測(cè) V 是否已關(guān)聯(lián) P,為真則讓 getView() 返回對(duì)應(yīng)的 V红且,否則返回 null坝茎。另外兩個(gè)方法負(fù)責(zé) V 和 P 的關(guān)聯(lián)與解關(guān)聯(lián),很簡(jiǎn)單暇番。
等等嗤放,你這不都是具體方法么,為啥還要弄成抽象類壁酬?待會(huì)自見分曉次酌。

應(yīng)用入口

新建一個(gè) MyApp 類,繼承 Application舆乔,用于獲取應(yīng)用全局的上下文岳服。

public class MyApp extends Application {
    private static MyApp instance;
    
    public static MyApp getInstance() {
        return instance;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        instance = this;
    }
}

這個(gè)類是你整個(gè)應(yīng)用的入口,一些你希望在應(yīng)用一跑起來(lái)就立即完成的工作(比如初始化一些三方庫(kù)希俩,包括 SDK)吊宋,可以寫入它的 onCreate() 方法。

切記不要用 instance = new MyApp() 一類的賦值去獲取實(shí)例颜武,這樣你得到的只是一個(gè)普通的 Java 類璃搜,不會(huì)具備任何 Application 的功能敞贡!

完成以后別忘了去 app 模塊的 AndroidManifest.xml羞海,在 Application 節(jié)點(diǎn)下添加一行——

android:name="com.example.mvp.MyApp"

網(wǎng)絡(luò)請(qǐng)求

前面已經(jīng)說過,網(wǎng)絡(luò)請(qǐng)求這類整體功能的封裝應(yīng)寫入框架褒墨,這樣應(yīng)用調(diào)用起來(lái)就很方便因块。這里用的請(qǐng)求庫(kù)是 Volley橘原,不夠了解的請(qǐng)看這篇文——

新建一個(gè) RequestManager 類,用于管理網(wǎng)絡(luò)請(qǐng)求吩愧。

public class RequestManager {
    private RequestQueue queue;
    private static volatile RequestManager instance;

    private RequestManager() {
        queue = Volley.newRequestQueue(MyApp.getInstance());
    }

    public static RequestManager getInstance() {
        if (instance == null) {
            synchronized (RequestManager.class) {
                if (instance == null) {
                    instance = new RequestManager();
                }
            }
        }
        return instance;
    }

    public RequestQueue getRequestQueue() {
        return queue;
    }
}

這里定義了一個(gè)請(qǐng)求隊(duì)列的對(duì)象芋酌,在構(gòu)造器里實(shí)例化,對(duì)象和構(gòu)造器均設(shè)為私有雁佳,只暴露兩個(gè) get 方法脐帝。因?yàn)檎?qǐng)求隊(duì)列一個(gè)便夠(多了很浪費(fèi)資源哦),這里采用了雙重校驗(yàn)鎖單例模式的寫法糖权。不了解單例模式請(qǐng)看——

下面定制我們的專屬網(wǎng)絡(luò)請(qǐng)求堵腹,網(wǎng)上大多數(shù) API 返回?cái)?shù)據(jù)都是 Json 對(duì)象,可以通過 Gson 很輕松的把它們轉(zhuǎn)換成 Java 對(duì)象星澳。新建一個(gè) MyRequest 類疚顷,繼承 Volley 里面的 Request 類。

public class MyRequest<T> extends Request<T> {

    private Gson mGSon;
    private Class<T> mClass;
    private Response.Listener<T> mListener;

    public MyRequest(String url, Class<T> clazz,
                     Response.Listener<T> listener, Response.ErrorListener errorListener) {
        this(Request.Method.GET, url, clazz, listener, errorListener);
    }

    public MyRequest(int method, String url, Class<T> clazz,
                     Response.Listener<T> listener, Response.ErrorListener errorListener) {
        super(method, url, errorListener);
        mGSon = new Gson();
        mClass = clazz;
        mListener = listener;
    }

    @Override
    protected Response<T> parseNetworkResponse(NetworkResponse response) {
        try {
            String json = new String(response.data,
                    HttpHeaderParser.parseCharset(response.headers));
            return Response.success(mGSon.fromJson(json, mClass),
                    HttpHeaderParser.parseCacheHeaders(response));
        } catch (UnsupportedEncodingException e) {
            return Response.error(new ParseError(e));
        }
    }

    @Override
    protected void deliverResponse(T response) {
        mListener.onResponse(response);
    }
}

代碼看著不少禁偎,其實(shí)很好理解腿堤。首先我們想要的 Java 對(duì)象不確定,所以用一個(gè)泛型 T 去描述如暖,并指定為與 Request 類的泛型相同笆檀。
構(gòu)造器是重寫自父類,里面實(shí)例化了馬上要講到的 Gson盒至,然后重載了一個(gè)不帶請(qǐng)求類型的酗洒,此時(shí)默認(rèn)請(qǐng)求類型為 GET。
接下來(lái)就是重寫 Request 類的 parseNetworkResponse() 和 ** deliverResponse()** 方法枷遂,前者用于解析請(qǐng)求到的響應(yīng)(也就是返回?cái)?shù)據(jù))寝蹈,后者用于將響應(yīng)傳遞給回調(diào)接口 mListener。解析時(shí)我們采用了 Gson登淘,它會(huì)強(qiáng)制我們處理 UnsupportedEncodingException箫老,最終返回的便是我們想要的 Java 對(duì)象。對(duì) Gson 不了解請(qǐng)看——

現(xiàn)在去處理響應(yīng)牲蜀,首先新建一個(gè)接口 MyListener——

public interface MyListener<T> {
    void onSuccess(T result);
    void onError(String errorMsg);    
}

這是一個(gè)回調(diào),成功時(shí)攜帶泛型描述的 Java 對(duì)象绅这,失敗時(shí)則攜帶錯(cuò)誤信息涣达。
然后補(bǔ)充前面的 RequestManager,添加發(fā)送 GET 和 POST 請(qǐng)求的封裝。

    public <T> void sendGet(String url, Class<T> clazz, final MyListener<T> listener) {
        MyRequest<T> request = new MyRequest<>(url, clazz, new Response.Listener<T>() {
            @Override
            public void onResponse(T response) {
                listener.onSuccess(response);
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                listener.onError(error.getMessage());
            }
        });
        addToRequestQueue(request);
    }

    public <T> void sendPost(String url, Class<T> clazz, final HashMap<String, String> map, final MyListener<T> listener) {
        MyRequest<T> request = new MyRequest<T>(Request.Method.POST, url, clazz, new Response.Listener<T>() {
            @Override
            public void onResponse(T response) {
                listener.onSuccess(response);
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                listener.onError(error.getMessage());
            }
        }) {
            @Override
            protected Map<String, String> getParams() throws AuthFailureError {
                return map;
            }
        };
        addToRequestQueue(request);
    }

    public <T> void addToRequestQueue(Request<T> req) {
        getRequestQueue().add(req);
    }

網(wǎng)絡(luò)請(qǐng)求搞定度苔!這里很明顯看出 Volley 的局限匆篓,就是不支持 POST 大數(shù)據(jù),因此不適合上傳文件(下載文件倒是可以通過 DownloadManager 實(shí)現(xiàn))寇窑。如果你的項(xiàng)目有上傳文件需求鸦概,應(yīng)該轉(zhuǎn)戰(zhàn) Retrofit 或 OkHttp。

圖片加載

這里只用 Volley 自帶的 ImageLoader 模塊實(shí)現(xiàn)圖片加載甩骏。該模塊性能不錯(cuò)窗市,但功能不如 Glide 一類的專業(yè)圖片加載框架豐富,大家可根據(jù)需求自行選擇合適的輪子饮笛。新手推薦看下面這篇文——

新建一個(gè) ImageUtil 類咨察,用于封裝圖片加載。

public class ImageUtil {
    public static void loadImage(String url, ImageView iv, int placeHolder, int errorHolder) {
        ImageLoader loader = new ImageLoader(
                RequestManager.getInstance().getRequestQueue(), new BitmapCache());
        if (iv instanceof NetworkImageView) {
            ((NetworkImageView) iv).setDefaultImageResId(placeHolder);
            ((NetworkImageView) iv).setErrorImageResId(errorHolder);
            ((NetworkImageView) iv).setImageUrl(url, loader);
        } else {
            ImageLoader.ImageListener listener = ImageLoader.getImageListener(iv,
                    placeHolder, errorHolder);
            loader.get(url, listener);
        }
    }

    private static class BitmapCache implements ImageLoader.ImageCache {
        private LruCache<String, Bitmap> cache;
        private final int maxSize = 10 * 1024 * 1024;//緩存大小設(shè)為10M

        BitmapCache() {
            cache = new LruCache<String, Bitmap>(maxSize) {
                @Override
                protected int sizeOf(String key, Bitmap value) {
                    return value.getByteCount() / 1024;
                }
            };
        }

        @Override
        public Bitmap getBitmap(String url) {
            return cache.get(url);
        }

        @Override
        public void putBitmap(String url, Bitmap bitmap) {
            cache.put(url, bitmap);
        }
    }
}

首先寫了一個(gè)內(nèi)部類 BitmapCache(因?yàn)楣ぞ哳悓?duì)外方法是靜態(tài)的福青,所以它也應(yīng)是靜態(tài))摄狱,實(shí)現(xiàn) Volley 的 ImageCache 接口并重寫方法。這里采用了 LruCache 實(shí)現(xiàn)圖片緩存素跺,不了解請(qǐng)看這篇文——

然后暴露一個(gè) loadImage() 方法給外部調(diào)用指厌。Volley 帶有一個(gè) 繼承自 ImageView 的控件 NetworkImageView刊愚,并有一套專屬的加載流程,因此在 loadImage() 方法里踩验,針對(duì)它和原生 ImageView 做了區(qū)分鸥诽。
OK,圖片加載也搞定了箕憾∧到瑁回首一看我們已寫了不少類和接口,整理一下吧袭异,如下圖示钠龙。這已經(jīng)是一個(gè)還算像樣的 MVP 快速開發(fā)框架了。

3.png

補(bǔ)充潤(rùn)色

繼續(xù)添加輪子御铃。我們都知道 MVP 的優(yōu)點(diǎn)碴里,但它也是有不少坑的——

  • 類爆炸,這也是 MVP 最受詬病之處上真。嚴(yán)格的 MVP 寫法下咬腋,每寫 1 個(gè)頁(yè)面(不算適配器和實(shí)體),要為之創(chuàng)建 8 個(gè)類睡互。
  • P 應(yīng)當(dāng)具備和 V 相似的生命周期根竿,但在眾多 V 里一個(gè)個(gè)調(diào)用 onAttach() 和 onDetach() 一個(gè)個(gè)關(guān)聯(lián)解關(guān)聯(lián)陵像,顯然是重復(fù)勞動(dòng)。
  • 有些 V 的展現(xiàn)內(nèi)容是共通的寇壳,比如進(jìn)度條醒颖、空白頁(yè)。

另外實(shí)際開發(fā)中我們還有一些需求九巡,簡(jiǎn)單列舉 2 個(gè)——

  • View 加載控件和數(shù)據(jù)的邏輯有時(shí)會(huì)很多图贸,混雜一起閱讀相當(dāng)不方便蹂季。
  • 應(yīng)用要求單擊返回鍵只彈出提示警告冕广,雙擊才是回到桌面。

現(xiàn)在我們就來(lái)解決它們偿洁。
首先在 util 目錄下新建兩個(gè)類撒汉,分別取名 ToastUtilReflectUtil

public class ToastUtil {
    private static Toast toast;
    public static void showToast(String text) {
        if (toast == null) {
            toast = Toast.makeText(MyApp.getInstance(), text, Toast.LENGTH_SHORT);
        } else {
            toast.setText(text);
        }
        toast.show();
    }
}

該類用于顯示一段土司(原生的接口有不妥之處涕滋,連續(xù)點(diǎn)擊會(huì)連續(xù)土司)睬辐。

public class ReflectUtil {
    public static <T> T getT(Object o, int i) {
        try {
            return ((Class<T>) ((ParameterizedType)
                    (o.getClass().getGenericSuperclass())).getActualTypeArguments()[i]).newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

該類則用于反射獲取指定泛型。
然后在 base 目錄下新建兩個(gè)抽象類 BaseActivityBaseMvpActivity宾肺,前者繼承 AppCompatActivity溯饵,并實(shí)現(xiàn)我們寫的 BaseView;后者繼承前者锨用。

public abstract class BaseActivity extends AppCompatActivity implements BaseView {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(getLayoutId());
        initView();
    }

    protected abstract int getLayoutId();
    protected abstract void initView();

    @Override
    public void showLoading() {

    }

    @Override
    public void hideLoading() {

    }

    @Override
    public void showError() {

    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        return checkBackAction() || super.onKeyDown(keyCode, event);
    }
    //雙擊退出相關(guān)
    private boolean mFlag = false;
    private long mTimeout = -1;
    private boolean checkBackAction() {
        long time = 3000L;//判定時(shí)間設(shè)為3秒
        boolean flag = mFlag;
        mFlag = true;
        boolean timeout = (mTimeout == -1 || (System.currentTimeMillis() - mTimeout) > time);
        if (mFlag && (mFlag != flag || timeout)) {
            mTimeout = System.currentTimeMillis();
            ToastUtil.showToast("再點(diǎn)擊一次回到桌面");
            return true;
        }
        return !mFlag;
    }
}

有時(shí)我們的活動(dòng)只是一個(gè)靜態(tài)的容器(比如歡迎頁(yè))丰刊,這時(shí)其實(shí)是沒必要使用 MVP 的。所以把包括 UI 的邏輯(雙擊退出)封裝在此增拥。BaseView 里面的方法也在此重寫啄巧,簡(jiǎn)明起見,就不具體實(shí)現(xiàn)了掌栅。
另外為了提升可讀性秩仆,BaseActivity 添加了兩個(gè)抽象方法 getLayoutId()initView()。子類在重寫時(shí)猾封,將前者的返回值改為布局 ID澄耍,在后者中進(jìn)行初始化(findViewById、setOnClickListener)即可晌缘。如果子類不在 onCreate() 方法里干其它事齐莲,重寫 onCreate() 一步也可以省略。

皮埃斯:如果你用了 ButterKnife枚钓、Dagger 等依賴注入框架铅搓,初始化和解綁(去 onDestory() 方法)工作同樣可以在這個(gè) BaseActivity 里完成。
有意思的是如果你在子類里用了 Android Studio 一款關(guān)于 ButterKnife 的助手插件(人氣很高的說)搀捷,它依然會(huì)很「認(rèn)真負(fù)責(zé)」的幫你重寫 onCreate() 和 onDestory()…… 只有自己動(dòng)手咔嚓掉了星掰。

public abstract class BaseMvpActivity
        <T extends BasePresenter, M extends BaseModel> extends BaseActivity {
    protected T mPresenter;
    protected M mModel;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mPresenter = ReflectUtil.getT(this, 0);
        mModel = ReflectUtil.getT(this, 1);
        mPresenter.onAttach(mModel, this);
    }

    @Override
    protected void onStart() {
        super.onStart();
        loadData();
    }
    
    protected abstract void loadData();

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mPresenter.onDetach();
    }
}

遇到動(dòng)態(tài)的多望,有數(shù)據(jù)請(qǐng)求和處理的頁(yè)面,再讓 MVP 出馬氢烘。這個(gè) BaseMvpActivity 繼承了 BaseActivity怀偷,因此包含了里面全部功能,同時(shí)又添加了一個(gè)抽象方法 loadData()播玖,有關(guān)數(shù)據(jù)交互的方法寫在里面即可椎工。

舉一反三,如果要讓碎片也能選擇性使用 MVP蜀踏,你應(yīng)該能寫出對(duì)應(yīng)的 BaseFragment 和 BaseMvpFragment 來(lái)了吧维蒙?

最后在 base 下創(chuàng)建接口 MvpListener,用于數(shù)據(jù)從 M 到 V 的層間傳遞果覆。

public interface MvpListener<T> {
    void onSuccess(T result);
    void onError(String errorMsg);
}

好了颅痊,屬于你的簡(jiǎn)易 MVP 快速開發(fā)框架已經(jīng)搭建完成,撒花慶祝一下吧局待。


4.png

開車上路

現(xiàn)在就在 app 模塊中寫個(gè)「知乎日?qǐng)?bào)」測(cè)試測(cè)試斑响,順便也學(xué)習(xí)一下 MVP 杜絕類爆炸的使用姿勢(shì)。簡(jiǎn)明起見钳榨,只用一個(gè) RecyclerView 請(qǐng)求今天的內(nèi)容(圖片 + 標(biāo)題)舰罚,不再涉及詳情。
首先創(chuàng)建知乎日?qǐng)?bào)的實(shí)體類 DailyBean薛耻。推薦用 Postman 做請(qǐng)求营罢,然后用 Android Studio 的插件 Gson Format 自動(dòng)生成。

public class DailyBean {
    private String date;
    private List<StoriesBean> stories;

    public String getDate() {
        return date;
    }
    public void setDate(String date) {
        this.date = date;
    }
    public List<StoriesBean> getStories() {
        return stories;
    }
    public void setStories(List<StoriesBean> stories) {
        this.stories = stories;
    }

    public static class StoriesBean {
        private int type;
        private int id;
        private String ga_prefix;
        private String title;
        private boolean multipic;
        private List<String> images;

        public int getType() {
            return type;
        }
        public void setType(int type) {
            this.type = type;
        }
        public int getId() {
            return id;
        }
        public void setId(int id) {
            this.id = id;
        }
        public String getGa_prefix() {
            return ga_prefix;
        }
        public void setGa_prefix(String ga_prefix) {
            this.ga_prefix = ga_prefix;
        }
        public String getTitle() {
            return title;
        }
        public void setTitle(String title) {
            this.title = title;
        }
        public boolean isMultipic() {
            return multipic;
        }
        public void setMultipic(boolean multipic) {
            this.multipic = multipic;
        }
        public List<String> getImages() {
            return images;
        }
        public void setImages(List<String> images) {
            this.images = images;
        }
    }
}

然后創(chuàng)建一個(gè)契約接口 DailyContract昭卓,這是 Google 推薦的類爆炸解決方案(不過筆者此處并沒嚴(yán)格按照官方要求去執(zhí)行)——

public interface DailyContract {
    interface DailyModel extends BaseModel {
        void loadDaily(String url, MvpListener<List<DailyBean.StoriesBean>> listener);
    }

    interface DailyView extends BaseView {
        void setData(List<DailyBean.StoriesBean> beanList);
    }

    abstract class DailyPresenter extends BasePresenter<DailyModel, DailyView> {
        protected abstract void loadData(String url);
    }
}

接口里同時(shí)承載了 Daily 這個(gè)模塊的 M愤钾,V 和 P(現(xiàn)在明白為何一開始要把 BasePresenter 弄成抽象類了吧),并且定義了方法規(guī)則候醒。
下面開始具體實(shí)現(xiàn)這三層能颁,首先是 P 層,創(chuàng)建一個(gè) DailyPresenterImpl 類倒淫,讓它繼承契約里面的 DailyPresenter伙菊。

public class DailyPresenterImpl extends DailyContract.DailyPresenter {
    @Override
    public void loadData(String url) {
        final DailyContract.DailyView mView = getView();
        if (mView == null) {
            return;
        }

        mView.showLoading();
        mModel.loadDaily(url, new MvpListener<List<DailyBean.StoriesBean>>() {
            @Override
            public void onSuccess(List<DailyBean.StoriesBean> result) {
                mView.hideLoading();
                mView.setData(result);
            }

            @Override
            public void onError(String errorMsg) {
                mView.hideLoading();
                mView.showError();
            }
        });
    }
}

邏輯很簡(jiǎn)單,首先拿到契約里 DailyView 的實(shí)例 mView敌土,做非空判斷镜硕,然后調(diào)用 showLoading() 方法顯示加載進(jìn)度條。
此后調(diào)用 mModel(也就是契約里 DailyModel 的實(shí)例)的 loadDaily() 方法返干,出結(jié)果后告知 mView兴枯,首先關(guān)閉進(jìn)度條。成功則執(zhí)行 setData() 展示數(shù)據(jù)矩欠,失敗則執(zhí)行 showError() 展示錯(cuò)誤信息财剖。
創(chuàng)建 DailyModelImpl 類悠夯,繼承契約里的 DailyModel。

public class DailyModelImpl implements DailyContract.DailyModel {
    @Override
    public void loadDaily(String url, final MvpListener<List<DailyBean.StoriesBean>> listener) {
        RequestManager.getInstance().sendGet(url, DailyBean.class, new MyListener<DailyBean>() {
            @Override
            public void onSuccess(DailyBean result) {
                listener.onSuccess(result.getStories());
            }

            @Override
            public void onError(String errorMsg) {
                listener.onError(errorMsg);
            }
        });
    }
}

這里具體實(shí)現(xiàn) loadDaily() 方法去請(qǐng)求數(shù)據(jù)躺坟,具體途徑當(dāng)然是之前我們封裝的網(wǎng)絡(luò)請(qǐng)求類沦补。成功則執(zhí)行 MvpListener 的成功回調(diào),失敗則執(zhí)行失敗回調(diào)咪橙。
創(chuàng)建我們用于展示的條目布局文件 item_daily夕膀。
這里我沒添加分割線,其實(shí)也不推薦直接在 item 里加分割線美侦。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:padding="8dp"
    android:layout_width="match_parent"
    android:layout_height="96dp">

    <com.android.volley.toolbox.NetworkImageView
        android:id="@+id/item_daily_iv"
        android:layout_width="80dp"
        android:layout_height="80dp"/>

    <TextView
        android:id="@+id/item_daily_tv"
        android:textSize="16sp"
        android:maxLines="3"
        android:ellipsize="end"
        android:gravity="center_vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_margin="8dp"/>

</LinearLayout>

這里插播 2 個(gè)小知識(shí)——

  • 在層級(jí)相同時(shí)产舞,F(xiàn)rameLayout 的性能略高于 LinearLayout,LinearLayout 又略高于RelativeLayout音榜。對(duì)應(yīng)的百分比布局同理庞瘸。
  • 約束布局能保證布局層級(jí)始終為 1捧弃,如果你的 item 很復(fù)雜赠叼,有必要考慮一下它。如果你不習(xí)慣拖拖拽拽违霞,可以先寫 XML 再轉(zhuǎn)換嘴办。

創(chuàng)建知乎日?qǐng)?bào)的適配器 DailyAdapter。這里我用了 RecyclerView买鸽,因?yàn)樗囊蕾囈呀?jīng)包含在了 mvp 里涧郊,app 里就不用再重復(fù)聲明了。

public class DailyAdapter extends RecyclerView.Adapter<DailyAdapter.DailyHolder> {
    private Context context;
    private List<DailyBean.StoriesBean> beanList;

    public DailyAdapter(Context context) {
        this.context = context;
        beanList = new ArrayList<>();
    }

    public void setBeanList(List<DailyBean.StoriesBean> list) {
        this.beanList.addAll(list);
        notifyDataSetChanged();
    }

    @Override
    public DailyHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new DailyHolder(LayoutInflater.from(context)
                .inflate(R.layout.item_daily, parent, false));
    }

    @Override
    public void onBindViewHolder(DailyHolder holder, int position) {
        DailyBean.StoriesBean bean = beanList.get(position);
        holder.tv.setText(bean.getTitle());
        ImageUtil.loadImage(bean.getImages().get(0), holder.iv,
                R.mipmap.ic_launcher_round, R.mipmap.ic_launcher_round);
    }

    @Override
    public int getItemCount() {
        return beanList.size();
    }

    static class DailyHolder extends RecyclerView.ViewHolder {
        TextView tv;
        NetworkImageView iv;

        DailyHolder(View itemView) {
            super(itemView);
            tv = (TextView) itemView.findViewById(R.id.item_daily_tv);
            iv = (NetworkImageView) itemView.findViewById(R.id.item_daily_iv);
        }
    }
}

簡(jiǎn)單起見我們只加載當(dāng)天的全部?jī)?nèi)容眼五。onBindViewHolder() 方法里面用到了之前封裝的圖片工具妆艘,占位圖就簡(jiǎn)單用小機(jī)器人代替了。
創(chuàng)建主界面的布局文件 activity_main看幼。

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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"
    tools:context="com.example.jin.mvpframetest.MainActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/ac_main_rcv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>

</FrameLayout>

創(chuàng)建一個(gè)日期工具類 DateUtil批旺,封裝日期格式化流程。
聰明如你诵姜,應(yīng)該知道這個(gè)類是放 app 更好汽煮,還是放 mvp 更好吧?

public class DateUtil {
    private static final Locale LOCALE = Locale.CHINA;
    public static String format(Date date, String s) {
        return new SimpleDateFormat(s, LOCALE).format(date);
    }
}

養(yǎng)成好習(xí)慣棚唆,創(chuàng)建一個(gè)類 Api暇赤,統(tǒng)一管理訪問接口。

public class Api {
    public static final String DAILY_HISTORY = "http://news.at.zhihu.com/api/4/news/before/";
}

最后寫展示用的類 MainActivity宵凌,也就是 MVP 的 V 層鞋囊。繼承 BaseMvpActivity 并實(shí)現(xiàn)契約里的 DailyView。

public class MainActivity extends BaseMvpActivity<DailyPresenterImpl, DailyModelImpl>
        implements DailyContract.DailyView {
    private DailyAdapter adapter;

    @Override
    protected int getLayoutId() {
        return R.layout.activity_main;
    }

    @Override
    protected void initView() {
        adapter = new DailyAdapter(this);
        RecyclerView rcv = (RecyclerView) findViewById(R.id.ac_main_rcv);
        rcv.setLayoutManager(new LinearLayoutManager(this));
        rcv.setHasFixedSize(true);
        rcv.setAdapter(adapter);
    }

    @Override
    protected void loadData() {
        mPresenter.loadData(Api.DAILY_HISTORY
                + DateUtil.format(new Date(), "yyyyMMdd"));
    }

    @Override
    public void setData(List<DailyBean.StoriesBean> beanList) {
        adapter.setBeanList(beanList);
    }
}

由于無(wú)須在活動(dòng)創(chuàng)建時(shí)做其它事瞎惫,onCreate() 方法可以不重寫了溜腐。其它 4 個(gè)重寫方法依次負(fù)責(zé)布局文件坯门,初始化控件,請(qǐng)求和展示數(shù)據(jù)逗扒,一目了然古戴。
最后別忘了在 AndroidManifest 里添加網(wǎng)絡(luò)訪問權(quán)限——

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

OK,可以跑應(yīng)用了~~


1.gif

實(shí)際效果比 gif 更好矩肩,Volley 做純閱讀應(yīng)用還是比較給力的现恼。
再看一看我們搭好框架后真正寫的代碼(筆者做了歸類整理)——


5.png

除去適配器和實(shí)體類,一個(gè)頁(yè)面我們只寫了 4 個(gè)類黍檩,有效解決了類爆炸叉袍;如果是類似歡迎頁(yè)那樣不涉及交互的,那直接繼承 BaseActivity 即可刽酱,不再用 MVP 模式寫了喳逛,這樣一個(gè)頁(yè)面只須寫 1 個(gè)類。
本文結(jié)束棵里,歡迎指教 and 拍磚~~
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末润文,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子殿怜,更是在濱河造成了極大的恐慌典蝌,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件头谜,死亡現(xiàn)場(chǎng)離奇詭異骏掀,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)柱告,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門截驮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人际度,你說我怎么就攤上這事葵袭。” “怎么了甲脏?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵眶熬,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我块请,道長(zhǎng)娜氏,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任墩新,我火速辦了婚禮贸弥,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘海渊。我一直安慰自己绵疲,他們只是感情好哲鸳,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著盔憨,像睡著了一般徙菠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上郁岩,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天婿奔,我揣著相機(jī)與錄音,去河邊找鬼问慎。 笑死萍摊,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的如叼。 我是一名探鬼主播冰木,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼笼恰!你這毒婦竟也來(lái)了踊沸?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤挖腰,失蹤者是張志新(化名)和其女友劉穎雕沿,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體猴仑,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年肥哎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了辽俗。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡篡诽,死狀恐怖崖飘,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情杈女,我是刑警寧澤朱浴,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站达椰,受9級(jí)特大地震影響翰蠢,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜啰劲,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一梁沧、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蝇裤,春花似錦廷支、人聲如沸频鉴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)垛孔。三九已至,卻和暖如春施敢,著一層夾襖步出監(jiān)牢的瞬間似炎,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工悯姊, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留羡藐,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓悯许,卻偏偏與公主長(zhǎng)得像仆嗦,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子先壕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

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

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,071評(píng)論 25 707
  • afinalAfinal是一個(gè)android的ioc瘩扼,orm框架 https://github.com/yangf...
    passiontim閱讀 15,429評(píng)論 2 45
  • 利用數(shù)據(jù)庫(kù)備份重新命名腳本文件:比喻為買來(lái)合法的泥巴,重新制作成殺人的槍垃僚。 利用00截?cái)嗌蟼鱳ebshell:比喻...
    我是一個(gè)好人嗎閱讀 299評(píng)論 0 0
  • 酸甜苦辣 讓我嘗盡一遍 悟出自己的智慧規(guī)則 經(jīng)歷過地獄 才知道何處是天堂 莫失莫忘 笑對(duì)人生
    無(wú)墨書生閱讀 228評(píng)論 0 2
  • 今天是大年初四谆棺,2017年也整整過去1個(gè)月了栽燕,如果說2016成長(zhǎng)為一棵樹的話,那么2017要像樹一樣成長(zhǎng)改淑。 一棵樹...
    長(zhǎng)毛兔呀閱讀 534評(píng)論 2 0