搭建自己的 Android MVP 快速開發框架
Android 開發進入「死丟丟」的時代後,引用三方庫在 Gradle 的支援下變得十分輕鬆。各種高手寫的開源框架,極大程度降低了新手入行(坑)的門檻,「一週開發一款 App 並上線」也不再遙不可及。
關於快速開發,筆者本人的意見是不一定什麼功能都自己寫,但框架最好是自己搭。雖然網上有很多非常成熟好用的完整框架,但直接「拿來主義」的話可能有 2 點不妥之處——
框架提供的功能你未必都用得到。比如你只寫一個純閱讀型別的應用(不帶大資料收藏功能),那麼你就用不到本地資料庫,這樣完整框架裡有關資料庫的內容,就給白白浪費了。
高手也有疏忽時,即便技術大牛,也不敢保證自己寫的程式碼沒有任何 bug,在任何使用場景都健壯堅挺。如果某天突然發現完整框架有什麼 bug 或者侷限,自己又沒能力解決,到頭來只能重構大塊內容甚至整個專案,這代價就非常大了。
綜上,筆者更傾向新手「站在巨人的肩膀上搭積木」,用高手寫的不同功能庫,自己動手搭屬於自己的快速開發框架。而且在搭框架的過程中,你能不知不覺中學到很多進階知識,對自己的成長也很有利。
限於水平和篇幅,筆者只用老牌輪子Volley做例子,搭一個僅涉及網路請求和圖片載入的MVP框架。當下最流行的原生框架應屬RxJava + Retrofit + OkHttp + Dagger,如果你想了解得更多,推薦下面幾篇文章——
當然,這些庫本質和 Volley 一樣,都是去實現具體功能的輪子,而MVP 的架構是不變的,所以下文的內容對它們同樣適用。
動手開始
開啟 Android Studio,新建一個專案MvpFrameTest
這時你會看到你的專案下面多了一個叫 mvp 的Module(和 app 一樣是加粗顯示的),不過角標是一個書架而非手機。這代表此模組是一個依賴庫,而非獨立執行的應用,我們今天主要的程式碼都寫在它裡面。
1.png
匯入依賴
點開mvp 下面的 build.gradle檔案(別錯搞成 app 下面的了哦),在dependency節點下面匯入我們要用的輪子——
compile'com.android.support:design:25.3.1'compile'com.android.volley:volley:1.0.0'compile'com.google.code.gson:gson:2.7'
這裡我希望內容儘量簡潔一點,因此只匯入設計適配(包含RecyclerView以及各種 Material Design 控制元件)、Volley(包含網路請求與圖片載入)和Gson(包含Json 解析)三個庫。語句後面的版本號僅供參考,因為當你看到這篇文章時,建議使用的版本號可能又變了。
下面點開app 下面的 build.gradle檔案,同樣在dependency節點下面新增依賴——
compile project(path:':mvp')
點選提示行裡面的Sync Now,一會兒任務完成,從此以後你在 mvp 裡面依賴的庫(包括自己寫的各種類),app 就都可以用了。
建議把整體性的功能諸如網路請求、圖片載入等寫在 mvp 裡,具體性的實現諸如 UI 配色、訪問地址寫在 app 裡,這樣你的框架使用起來才更加靈活。
如果你對 Gradle 還不瞭解,推薦一篇文——
MVP
下面開始寫自己的東西了,由於我們想要的是 MVP 設計模式,首先應該完成通用的 M、V 和 P。對 MVP 不瞭解的推薦一篇文——
找到 mvp 下面的com.example.mvp包,如圖所示
2.png
在裡面新建兩個介面(Interface),分別取名BaseView和BaseModel。
publicinterfaceBaseView{voidshowLoading();voidhideLoading();voidshowError();}
BaseView 裡面我們定義了三個抽象方法,分別用於顯示載入、隱藏載入和顯示載入失敗的內容。這些方法最終會交給你的檢視(也就是 Activity 或者 Fragment)去實現。
publicinterfaceBaseModel{}
BaseModel 裡面目前可以什麼都不寫。如果你參與一個團隊開發,介面和資料有比較統一的格式,那可以在此做一些規範工作。
OK,M 和 V 都有了,再新建一個抽象類,取名BasePresenter。
publicabstractclassBasePresenter{protectedM mModel;protectedWeakReference mViewRef;protectedvoidonAttach(M model, V view){ mModel = model; mViewRef =newWeakReference<>(view); }protectedVgetView(){returnisViewAttached() ? mViewRef.get() :null; }protectedbooleanisViewAttached(){returnnull!= mViewRef &&null!= mViewRef.get(); }protectedvoidonDetach(){if(null!= mViewRef) { mViewRef.clear(); mViewRef =null; } }}
首先聲明瞭兩個泛型 M 和 V,M 對應要處理的 Model,V 則對應負責展示的View。由於 V 一般比較大,這裡採用了弱引用的寫法,避免記憶體洩漏。
isViewAttached()用於檢測 V 是否已關聯 P,為真則讓getView()返回對應的 V,否則返回 null。另外兩個方法負責 V 和 P 的關聯與解關聯,很簡單。
等等,你這不都是具體方法麼,為啥還要弄成抽象類?待會自見分曉。
應用入口
新建一個MyApp類,繼承 Application,用於獲取應用全域性的上下文。
publicclassMyAppextendsApplication{privatestaticMyApp instance;publicstaticMyAppgetInstance(){returninstance; }@OverridepublicvoidonCreate(){super.onCreate(); instance =this; }}
這個類是你整個應用的入口,一些你希望在應用一跑起來就立即完成的工作(比如初始化一些三方庫,包括 SDK),可以寫入它的 onCreate() 方法。
切記不要用 instance = new MyApp() 一類的賦值去獲取例項,這樣你得到的只是一個普通的 Java 類,不會具備任何 Application 的功能!
完成以後別忘了去app 模組的 AndroidManifest.xml,在 Application 節點下新增一行——
android:name="com.example.mvp.MyApp"
網路請求
前面已經說過,網路請求這類整體功能的封裝應寫入框架,這樣應用呼叫起來就很方便。這裡用的請求庫是Volley,不夠了解的請看這篇文——
這是一個系列文,共四篇,新手建議看完前三篇。
新建一個RequestManager類,用於管理網路請求。
publicclassRequestManager{privateRequestQueuequeue;privatestaticvolatileRequestManager instance;privateRequestManager(){queue= Volley.newRequestQueue(MyApp.getInstance()); }publicstaticRequestManagergetInstance(){if(instance == null) { synchronized (RequestManager.class) {if(instance == null) { instance =newRequestManager(); } } }returninstance; }publicRequestQueuegetRequestQueue(){returnqueue; }}
這裡定義了一個請求佇列的物件,在構造器裡實例化,物件和構造器均設為私有,只暴露兩個 get 方法。因為請求佇列一個便夠(多了很浪費資源哦),這裡採用了雙重校驗鎖單例模式的寫法。不瞭解單例模式請看——
下面定製我們的專屬網路請求,網上大多數 API 返回資料都是 Json 物件,可以通過 Gson 很輕鬆的把它們轉換成 Java 物件。新建一個MyRequest類,繼承 Volley 裡面的 Request 類。
publicclassMyRequestextendsRequest{privateGson mGSon;privateClass mClass;privateResponse.Listener mListener;publicMyRequest(String url, Class clazz,
Response.Listener listener, Response.ErrorListener errorListener){this(Request.Method.GET, url, clazz, listener, errorListener); }publicMyRequest(intmethod, String url, Class clazz, Response.Listener listener, Response.ErrorListener errorListener){super(method, url, errorListener); mGSon =newGson(); mClass = clazz; mListener = listener; }@OverrideprotectedResponseparseNetworkResponse(NetworkResponse response){try{ String json =newString(response.data, HttpHeaderParser.parseCharset(response.headers));returnResponse.success(mGSon.fromJson(json, mClass), HttpHeaderParser.parseCacheHeaders(response)); }catch(UnsupportedEncodingException e) {returnResponse.error(newParseError(e)); } }@OverrideprotectedvoiddeliverResponse(T response){ mListener.onResponse(response); }}
程式碼看著不少,其實很好理解。首先我們想要的 Java 物件不確定,所以用一個泛型 T 去描述,並指定為與 Request 類的泛型相同。
構造器是重寫自父類,裡面例項化了馬上要講到的 Gson,然後過載了一個不帶請求型別的,此時預設請求型別為 GET。
接下來就是重寫 Request 類的parseNetworkResponse()和 ** deliverResponse()** 方法,前者用於解析請求到的響應(也就是返回資料),後者用於將響應傳遞給回撥介面mListener。解析時我們採用了Gson,它會強制我們處理UnsupportedEncodingException,最終返回的便是我們想要的 Java 物件。對 Gson 不瞭解請看——
這是一個系列文,共四篇,新手可以只看第一篇。
現在去處理響應,首先新建一個介面MyListener——
publicinterfaceMyListener{voidonSuccess(T result);voidonError(String errorMsg); }
這是一個回撥,成功時攜帶泛型描述的 Java 物件,失敗時則攜帶錯誤資訊。
然後補充前面的 RequestManager,添加發送 GET 和 POST 請求的封裝。
publicvoidsendGet(String url, Class clazz,finalMyListener listener){ MyRequest request =newMyRequest<>(url, clazz,newResponse.Listener() {@OverridepublicvoidonResponse(T response){ listener.onSuccess(response); } },newResponse.ErrorListener() {@OverridepublicvoidonErrorResponse(VolleyError error){ listener.onError(error.getMessage()); } }); addToRequestQueue(request); }publicvoidsendPost(String url, Class clazz,finalHashMap map,finalMyListener listener){ MyRequest request =newMyRequest(Request.Method.POST, url, clazz,newResponse.Listener() {@OverridepublicvoidonResponse(T response){ listener.onSuccess(response); } },newResponse.ErrorListener() {@OverridepublicvoidonErrorResponse(VolleyError error){ listener.onError(error.getMessage()); } }) {@OverrideprotectedMapgetParams()throwsAuthFailureError{returnmap; } }; addToRequestQueue(request); }publicvoidaddToRequestQueue(Request req){ getRequestQueue().add(req); }
網路請求搞定!這裡很明顯看出 Volley 的侷限,就是不支援 POST 大資料,因此不適合上傳檔案(下載檔案倒是可以通過 DownloadManager 實現)。如果你的專案有上傳檔案需求,應該轉戰 Retrofit 或 OkHttp。
圖片載入
這裡只用 Volley 自帶的 ImageLoader 模組實現圖片載入。該模組效能不錯,但功能不如 Glide 一類的專業圖片載入框架豐富,大家可根據需求自行選擇合適的輪子。新手推薦看下面這篇文——
新建一個ImageUtil類,用於封裝圖片載入。
publicclassImageUtil{publicstaticvoidloadImage(String url, ImageView iv,intplaceHolder,interrorHolder){ ImageLoader loader =newImageLoader( RequestManager.getInstance().getRequestQueue(),newBitmapCache());if(ivinstanceofNetworkImageView) { ((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); } }privatestaticclassBitmapCacheimplementsImageLoader.ImageCache{privateLruCache cache;privatefinalintmaxSize =10*1024*1024;//快取大小設為10MBitmapCache() { cache =newLruCache(maxSize) {@OverrideprotectedintsizeOf(String key, Bitmap value){returnvalue.getByteCount() /1024; } }; }@OverridepublicBitmapgetBitmap(String url){returncache.get(url); }@OverridepublicvoidputBitmap(String url, Bitmap bitmap){ cache.put(url, bitmap); } }}
首先寫了一個內部類BitmapCache(因為工具類對外方法是靜態的,所以它也應是靜態),實現 Volley 的 ImageCache 介面並重寫方法。這裡採用了 LruCache 實現圖片快取,不瞭解請看這篇文——
然後暴露一個loadImage()方法給外部呼叫。Volley 帶有一個 繼承自 ImageView 的控制元件NetworkImageView,並有一套專屬的載入流程,因此在 loadImage() 方法裡,針對它和原生 ImageView 做了區分。
OK,圖片載入也搞定了。回首一看我們已寫了不少類和介面,整理一下吧,如下圖示。這已經是一個還算像樣的 MVP 快速開發框架了。
3.png
補充潤色
繼續新增輪子。我們都知道 MVP 的優點,但它也是有不少坑的——
類爆炸,這也是 MVP 最受詬病之處。嚴格的 MVP 寫法下,每寫 1 個頁面(不算介面卡和實體),要為之建立 8 個類。
P 應當具備和 V 相似的生命週期,但在眾多 V 裡一個個呼叫 onAttach() 和 onDetach() 一個個關聯解關聯,顯然是重複勞動。
有些 V 的展現內容是共通的,比如進度條、空白頁。
另外實際開發中我們還有一些需求,簡單列舉 2 個——
View 載入控制元件和資料的邏輯有時會很多,混雜一起閱讀相當不方便。
應用要求單擊返回鍵只彈出提示警告,雙擊才是回到桌面。
現在我們就來解決它們。
首先在 util 目錄下新建兩個類,分別取名ToastUtil和ReflectUtil。
publicclassToastUtil{privatestaticToast toast;publicstaticvoidshowToast(String text){if(toast ==null) { toast = Toast.makeText(MyApp.getInstance(), text, Toast.LENGTH_SHORT); }else{ toast.setText(text); } toast.show(); }}
該類用於顯示一段土司(原生的介面有不妥之處,連續點選會連續土司)。
publicclassReflectUtil{publicstaticTgetT(Object o,inti){try{return((Class) ((ParameterizedType) (o.getClass().getGenericSuperclass())).getActualTypeArguments()[i]).newInstance(); }catch(Exception e) { e.printStackTrace(); }returnnull; }}
該類則用於反射獲取指定泛型。
然後在 base 目錄下新建兩個抽象類BaseActivity和BaseMvpActivity,前者繼承 AppCompatActivity,並實現我們寫的 BaseView;後者繼承前者。
publicabstractclassBaseActivityextendsAppCompatActivityimplementsBaseView{@OverrideprotectedvoidonCreate(@Nullable Bundle savedInstanceState){super.onCreate(savedInstanceState); setContentView(getLayoutId()); initView(); }protectedabstractintgetLayoutId();protectedabstractvoidinitView();@OverridepublicvoidshowLoading(){ }@OverridepublicvoidhideLoading(){ }@OverridepublicvoidshowError(){ }@OverridepublicbooleanonKeyDown(intkeyCode, KeyEvent event){returncheckBackAction() ||super.onKeyDown(keyCode, event); }//雙擊退出相關privatebooleanmFlag =false;privatelongmTimeout = -1;privatebooleancheckBackAction(){longtime =3000L;//判定時間設為3秒booleanflag = mFlag; mFlag =true;booleantimeout = (mTimeout == -1|| (System.currentTimeMillis() - mTimeout) > time);if(mFlag && (mFlag != flag || timeout)) { mTimeout = System.currentTimeMillis(); ToastUtil.showToast("再點選一次回到桌面");returntrue; }return!mFlag; }}
有時我們的活動只是一個靜態的容器(比如歡迎頁),這時其實是沒必要使用 MVP 的。所以把包括 UI 的邏輯(雙擊退出)封裝在此。BaseView 裡面的方法也在此重寫,簡明起見,就不具體實現了。
另外為了提升可讀性,BaseActivity 添加了兩個抽象方法getLayoutId()和initView()。子類在重寫時,將前者的返回值改為佈局 ID,在後者中進行初始化(findViewById、setOnClickListener)即可。如果子類不在 onCreate() 方法裡幹其它事,重寫 onCreate() 一步也可以省略。
皮埃斯:如果你用了 ButterKnife、Dagger 等依賴注入框架,初始化和解綁(去 onDestory() 方法)工作同樣可以在這個 BaseActivity 裡完成。
有意思的是如果你在子類裡用了 Android Studio 一款關於 ButterKnife 的助手外掛(人氣很高的說),它依然會很「認真負責」的幫你重寫 onCreate() 和 onDestory()…… 只有自己動手咔嚓掉了。
publicabstractclassBaseMvpActivityextendsBaseActivity{protectedT mPresenter;protectedM mModel;@OverrideprotectedvoidonCreate(@Nullable Bundle savedInstanceState){super.onCreate(savedInstanceState); mPresenter = ReflectUtil.getT(this,0); mModel = ReflectUtil.getT(this,1); mPresenter.onAttach(mModel,this); }@OverrideprotectedvoidonStart(){super.onStart(); loadData(); }protectedabstractvoidloadData();@OverrideprotectedvoidonDestroy(){super.onDestroy(); mPresenter.onDetach(); }}
遇到動態的,有資料請求和處理的頁面,再讓 MVP 出馬。這個 BaseMvpActivity 繼承了 BaseActivity,因此包含了裡面全部功能,同時又添加了一個抽象方法loadData(),有關資料互動的方法寫在裡面即可。
舉一反三,如果要讓碎片也能選擇性使用 MVP,你應該能寫出對應的 BaseFragment 和 BaseMvpFragment 來了吧?
最後在 base 下建立介面MvpListener,用於資料從 M 到 V 的層間傳遞。
publicinterfaceMvpListener{voidonSuccess(T result);voidonError(String errorMsg);}
好了,屬於你的簡易 MVP 快速開發框架已經搭建完成,撒花慶祝一下吧。
4.png
開車上路
現在就在 app 模組中寫個「知乎日報」測試測試,順便也學習一下 MVP 杜絕類爆炸的使用姿勢。簡明起見,只用一個 RecyclerView 請求今天的內容(圖片 + 標題),不再涉及詳情。
首先建立知乎日報的實體類DailyBean。推薦用Postman做請求,然後用 Android Studio 的外掛Gson Format自動生成。
publicclassDailyBean{privateString date;privateList stories;publicStringgetDate(){returndate; }publicvoidsetDate(String date){this.date = date; }publicListgetStories(){returnstories; }publicvoidsetStories(List stories){this.stories = stories; }publicstaticclassStoriesBean{privateinttype;privateintid;privateString ga_prefix;privateString title;privatebooleanmultipic;privateList images;publicintgetType(){returntype; }publicvoidsetType(inttype){this.type = type; }publicintgetId(){returnid; }publicvoidsetId(intid){this.id = id; }publicStringgetGa_prefix(){returnga_prefix; }publicvoidsetGa_prefix(String ga_prefix){this.ga_prefix = ga_prefix; }publicStringgetTitle(){returntitle; }publicvoidsetTitle(String title){this.title = title; }publicbooleanisMultipic(){returnmultipic; }publicvoidsetMultipic(booleanmultipic){this.multipic = multipic; }publicListgetImages(){returnimages; }publicvoidsetImages(List images){this.images = images; } }}
然後建立一個契約介面DailyContract,這是 Google 推薦的類爆炸解決方案(不過筆者此處並沒嚴格按照官方要求去執行)——
publicinterfaceDailyContract{interfaceDailyModelextendsBaseModel{voidloadDaily(String url, MvpListener> listener); }interfaceDailyViewextendsBaseView{voidsetData(List beanList); }abstractclassDailyPresenterextendsBasePresenter{protectedabstractvoidloadData(String url); }}
接口裡同時承載了 Daily 這個模組的 M,V 和 P(現在明白為何一開始要把 BasePresenter 弄成抽象類了吧),並且定義了方法規則。
下面開始具體實現這三層,首先是 P 層,建立一個DailyPresenterImpl類,讓它繼承契約裡面的 DailyPresenter。
publicclassDailyPresenterImplextendsDailyContract.DailyPresenter{@OverridepublicvoidloadData(String url){finalDailyContract.DailyView mView = getView();if(mView ==null) {return; } mView.showLoading(); mModel.loadDaily(url,newMvpListener>() {@OverridepublicvoidonSuccess(List result){ mView.hideLoading(); mView.setData(result); }@OverridepublicvoidonError(String errorMsg){ mView.hideLoading(); mView.showError(); } }); }}
邏輯很簡單,首先拿到契約裡 DailyView 的例項 mView,做非空判斷,然後呼叫 showLoading() 方法顯示載入進度條。
此後呼叫 mModel(也就是契約裡 DailyModel 的例項)的 loadDaily() 方法,出結果後告知 mView,首先關閉進度條。成功則執行 setData() 展示資料,失敗則執行 showError() 展示錯誤資訊。
建立DailyModelImpl類,繼承契約裡的 DailyModel。
publicclassDailyModelImplimplementsDailyContract.DailyModel{@OverridepublicvoidloadDaily(String url,finalMvpListener> listener){ RequestManager.getInstance().sendGet(url, DailyBean.class,newMyListener() {@OverridepublicvoidonSuccess(DailyBean result){ listener.onSuccess(result.getStories()); }@OverridepublicvoidonError(String errorMsg){ listener.onError(errorMsg); } }); }}
這裡具體實現 loadDaily() 方法去請求資料,具體途徑當然是之前我們封裝的網路請求類。成功則執行 MvpListener 的成功回撥,失敗則執行失敗回撥。
建立我們用於展示的條目佈局檔案item_daily。
這裡我沒新增分割線,其實也不推薦直接在 item 里加分割線。
這裡插播 2 個小知識——
在層級相同時,FrameLayout 的效能略高於 LinearLayout,LinearLayout 又略高於RelativeLayout。對應的百分比佈局同理。
約束佈局能保證佈局層級始終為 1,如果你的 item 很複雜,有必要考慮一下它。如果你不習慣拖拖拽拽,可以先寫 XML 再轉換。
建立知乎日報的介面卡DailyAdapter。這裡我用了 RecyclerView,因為它的依賴已經包含在了 mvp 裡,app 裡就不用再重複聲明瞭。
publicclassDailyAdapterextendsRecyclerView.Adapter{privateContext context;privateList beanList;publicDailyAdapter(Context context){this.context = context; beanList =newArrayList<>(); }publicvoidsetBeanList(List list){this.beanList.addAll(list); notifyDataSetChanged(); }@OverridepublicDailyHolderonCreateViewHolder(ViewGroup parent,intviewType){returnnewDailyHolder(LayoutInflater.from(context) .inflate(R.layout.item_daily, parent,false)); }@OverridepublicvoidonBindViewHolder(DailyHolder holder,intposition){ 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); }@OverridepublicintgetItemCount(){returnbeanList.size(); }staticclassDailyHolderextendsRecyclerView.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); } }}
簡單起見我們只加載當天的全部內容。onBindViewHolder() 方法裡面用到了之前封裝的圖片工具,佔位圖就簡單用小機器人代替了。
建立主介面的佈局檔案activity_main。
建立一個日期工具類DateUtil,封裝日期格式化流程。
聰明如你,應該知道這個類是放 app 更好,還是放 mvp 更好吧?
publicclassDateUtil{privatestaticfinalLocale LOCALE = Locale.CHINA;publicstaticStringformat(Date date, String s){returnnewSimpleDateFormat(s, LOCALE).format(date); }}
養成好習慣,建立一個類Api,統一管理訪問介面。
publicclassApi{publicstaticfinalString DAILY_HISTORY ="http://news.at.zhihu.com/api/4/news/before/";}
最後寫展示用的類MainActivity,也就是 MVP 的 V 層。繼承 BaseMvpActivity 並實現契約裡的 DailyView。
publicclassMainActivityextendsBaseMvpActivityimplementsDailyContract.DailyView{privateDailyAdapter adapter;@OverrideprotectedintgetLayoutId(){returnR.layout.activity_main; }@OverrideprotectedvoidinitView(){ adapter =newDailyAdapter(this); RecyclerView rcv = (RecyclerView) findViewById(R.id.ac_main_rcv); rcv.setLayoutManager(newLinearLayoutManager(this)); rcv.setHasFixedSize(true); rcv.setAdapter(adapter); }@OverrideprotectedvoidloadData(){ mPresenter.loadData(Api.DAILY_HISTORY + DateUtil.format(newDate(),"yyyyMMdd")); }@OverridepublicvoidsetData(List beanList){ adapter.setBeanList(beanList); }}
由於無須在活動建立時做其它事,onCreate() 方法可以不重寫了。其它 4 個重寫方法依次負責佈局檔案,初始化控制元件,請求和展示資料,一目瞭然。
最後別忘了在AndroidManifest裡新增網路訪問許可權——
OK,可以跑應用了~~
1.gif
實際效果比 gif 更好,Volley 做純閱讀應用還是比較給力的。
再看一看我們搭好框架後真正寫的程式碼(筆者做了歸類整理)——
5.png
除去介面卡和實體類,一個頁面我們只寫了 4 個類,有效解決了類爆炸;如果是類似歡迎頁那樣不涉及互動的,那直接繼承 BaseActivity 即可,不再用 MVP 模式寫了,這樣一個頁面只須寫 1 個類。
本文結束,歡迎指教 and 拍磚~~
作者:彼岸sakura
連結:https://www.jianshu.com/p/965e67222454
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。