開發(fā)一個簡易的干貨客戶端

前言

前一段時間在微博上看到了一個面試題,要求一定時間內(nèi)開發(fā)一個簡易的 Gank.io 客戶端,雖說筆者并無求職意向,但作為練手感覺也很不錯,就嘗試了一下。

GitHub Repo: unixzii / Android-Proficiency-Exercise
建議大家對照代碼閱讀本文!!

App 運行截圖如下:


題目要求:

  • 可以調(diào)用 API 獲取數(shù)據(jù)
  • 異步加載圖片并緩存
  • 下拉刷新,上拉加載更多
  • 可以將數(shù)據(jù)緩存到數(shù)據(jù)庫以供離線瀏覽

其實乍一看是一個非常簡單的小項目,我在 5 個小時內(nèi)快速寫出了一個版本,并向原 Repo 發(fā)起了 PR,但是這一版的代碼僅僅是完成功能,也就是實現(xiàn)題目要求的界面和功能,但是性能和代碼并不是最優(yōu)狀態(tài)。今天又花了近 4 個小時進行了一次重構(gòu),目前來說是一個比較完美的狀態(tài)了。

開發(fā)模式

筆者是 iOS 開發(fā)者,Android 僅僅是作為業(yè)余愛好來研究,但應(yīng)用開發(fā)歸根到底都是相通的,唯一不同的就是 API 及平臺差異。

這個 app 我采用了很常見的 MVP 開發(fā)模式。將應(yīng)用分為了三大模塊,分別是:

  • Main(包括 Toolbar、TabLayout、Fragment Container),這部分負責協(xié)調(diào)初始化子模塊,并加載標簽。
  • Entity(Main 選中標簽所對應(yīng)的內(nèi)容界面),這部分被 Main 所復用,用于加載展示數(shù)據(jù)。
  • WebView,這部分我很簡單地實現(xiàn)了一下,主要是用來展示文章的。

對于每一個模塊,我都寫了一個 Contract 契約類,它用來規(guī)定 Presenter 與 View 之間的交互,契約類中包括 PresenterView 兩個內(nèi)部接口,這樣就非常有利于模塊的多人協(xié)作開發(fā),各部分相互獨立,各部分開發(fā)時不需了解其它部分的實現(xiàn),只需調(diào)用接口中方法即可。

View & Presenter

我們來看 Entity 模塊的 Contract 類:

public interface EntityContract {

    interface View {

        int STATE_LOADING_IDLE = 0;
        int STATE_LOADING_REFRESHING = 1;
        int STATE_LOADING_RESERVING = 2;

        void setLoading(int state);

        void addEntities(List<Entity> entities);

        void clearEntities();

        void showNetworkError();

        void runOnUiThread(Runnable runnable);

    }

    abstract class Presenter extends BasePresenter<View> {

        abstract void setCategory(String categoryName);

        abstract String getCategory();

        abstract void refresh();

        abstract void reserve();

    }

}

View 和 Presenter 所提供的功能一目了然,這里的 BasePresenter 是一個范型抽象類,其中實現(xiàn)了 attaching 和 detaching 時的相關(guān)處理,并提供了獲取 View 的便利方法。

這里的 View 我才用 Fragment 來實現(xiàn),列表選用了 RecyclerView,事實上列表控件也屬于一個小的 MVC 組件,Adapter 作為 Model,那么我們?nèi)绾渭毣@部分的實現(xiàn)呢?整個 app 的 Model 層我寫的比較簡單,基本就是 Bean 類,完全可以直接交給 View 層作為一個 ViewModel,而 View 層的接口也十分簡明,除了一些狀態(tài)接口外,剩下的就是操作這些 ViewModel 的接口,我們可以向 View 中添加 ViewModels 也可以清空。對于已經(jīng)添加進視圖的 ViewModels,View 就負責展示這些數(shù)據(jù)即可,無需與 Presenter 進行再次交互。

那么 Presenter 的職責也很分明,就是負責加載數(shù)據(jù),處理數(shù)據(jù),做緩存處理等工作的。創(chuàng)建 Presenter 的時候我們無須操心 Context 的問題,我創(chuàng)建了一個 Application 單例,Presenter 沒有要進行 UI 操作的必要,因此使用 Application Context 就可以滿足數(shù)據(jù)庫、磁盤、網(wǎng)絡(luò)等操作。創(chuàng)建 Presenter 的時候我們可以配置好所需的 Retrofit、OkHttpClient、數(shù)據(jù)庫助手類等對象,當然這里我們也可以使用單例,然后用 Dagger2 注入進去。

然后我們來看看緩存的處理:
在 Presenter 被 attach 到 View 上時,我們進行緩存的加載,因為這時 View 一定是空狀態(tài),所以我們執(zhí)行的加載邏輯:


Presenter 緩存是一個存在于 Presenter 類內(nèi)部的數(shù)組列表,是一級緩存,F(xiàn)ragment destroyView 后,Presenter 類并不會銷毀,因此在 Fragment 再次 createView 的時候,我們可以直接拿出其中緩存的數(shù)據(jù)供 View 顯示。

如果 Presenter 中沒有緩存,則嘗試從數(shù)據(jù)庫讀取緩存數(shù)據(jù)(作為二級緩存),如果數(shù)據(jù)庫也不存在緩存數(shù)據(jù)才進行網(wǎng)絡(luò)請求。

網(wǎng)絡(luò) & 緩存

網(wǎng)絡(luò)請求方面采用了 Retrofit + RxJava + Gson,這方面的文章其實很多,本文不想多贅述。這里談?wù)劸彺妫F(xiàn)在的緩存無非分為兩大種:

  • 數(shù)據(jù)庫緩存
  • Response 緩存

數(shù)據(jù)庫緩存稍微麻煩一些,但是好處也是顯而易見的,數(shù)據(jù)讀寫更加靈活,可控性更強;Response 緩存就是將服務(wù)器的響應(yīng)(JSON、XML、BLOB、Protobuf 等)用 Hash 算法進行存儲,下次請求相同的 API 時就能直接拿到,好處就是實現(xiàn)簡單,加載邏輯不需要做區(qū)分處理,每次都按照網(wǎng)絡(luò)請求來處理即可,但是缺點就是不夠靈活,可擴展性差。

我采用了數(shù)據(jù)庫存儲,簡單地封裝了 Android 中原生的數(shù)據(jù)庫操作類,然后寫了一個 DAO 來隱藏 SQL 實現(xiàn)細節(jié),因為有些干貨數(shù)據(jù)中含有圖片數(shù)組,因此一張表肯定是不夠的,我們需要構(gòu)建第二張表來保存圖片 URL 數(shù)據(jù)。由于我們不會直接操作這些圖片 URL 數(shù)據(jù),因此這張圖片表的操作可以與干貨表一同被封裝到一個 DAO 中,這樣一來 Presenter 對數(shù)據(jù)庫的操作也變得十分簡單了。

最終整個 Gank.io App 的架構(gòu)如下,非常簡潔清晰:


總結(jié)

整個應(yīng)用開發(fā)下來也遇到了很多小問題,雖然應(yīng)用需求十分簡單,但是能熟練地在有限時間內(nèi)開發(fā)完這樣一個小應(yīng)用也并非一件十分簡單的事,它要求我們站在一個更高地層面去設(shè)計整個應(yīng)用的架構(gòu)、業(yè)務(wù)邏輯等。在長時間與 API 和細節(jié)功能打交道的同時,偶爾也需要做一做這樣的小軟件,每次都會有不同的收獲。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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