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 害捕,如果你想了解得更多,推薦下面幾篇文章——
- 給 Android 開發(fā)者的 RxJava 詳解
- RxJava 與 Retrofit 結(jié)合的最佳實(shí)踐
- Dagger 2 從入門到放棄再到恍然大悟
- MVP + Dagger2 + Retrofit 實(shí)現(xiàn)更清晰的架構(gòu)
當(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)用谭确,我們今天主要的代碼都寫在它里面帘营。
導(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 包厚柳,如圖所示
在里面新建兩個(gè)接口(Interface)氧枣,分別取名 BaseView 和 BaseModel 。
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)看這篇文——
-
Android Volley 完全解析
這是一個(gè)系列文,共四篇涡上,新手建議看完前三篇趾断。
新建一個(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)看——
-
你真的會(huì)用 Gson 嗎黔州?Gson 使用指南
這是一個(gè)系列文耍鬓,共四篇,新手可以只看第一篇流妻。
現(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ā)框架了。
補(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è)類撒汉,分別取名 ToastUtil 和 ReflectUtil。
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è)抽象類 BaseActivity 和 BaseMvpActivity宾肺,前者繼承 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)搭建完成,撒花慶祝一下吧局待。
開車上路
現(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)用了~~
實(shí)際效果比 gif 更好矩肩,Volley 做純閱讀應(yīng)用還是比較給力的现恼。
再看一看我們搭好框架后真正寫的代碼(筆者做了歸類整理)——
除去適配器和實(shí)體類,一個(gè)頁(yè)面我們只寫了 4 個(gè)類黍檩,有效解決了類爆炸叉袍;如果是類似歡迎頁(yè)那樣不涉及交互的,那直接繼承 BaseActivity 即可刽酱,不再用 MVP 模式寫了喳逛,這樣一個(gè)頁(yè)面只須寫 1 個(gè)類。
本文結(jié)束棵里,歡迎指教 and 拍磚~~