KCommon-使用Kotlin編寫,基於MVP的極速開發框架

KCommon-使用Kotlin編寫,基於MVP的極速開發框架

咱們在開發Android應用程序的時候其實會有不少通用的代碼,比方說很常見的頁面的幾種基本狀態的切換:正常、加載失敗、加載中、空頁面。又或者是下拉刷新和若是數據須要分頁而帶來的上拉加載更多數據等等操做。固然,這其中最繁瑣的仍是關於MVP相關模板代碼的編寫,熟悉Android中MVP架構的小夥伴們應該都知道,嚴格按照MVP架構的話,咱們每個Activity或者Fragment都須要多寫一個接口和兩個實現類:MVPContract、MVPModel和MVPPresenter。而這些Contract、Model和Presenter又不近類似,因此在我以前的開發中,若是一個新的APP有30個頁面,那麼加上這些MVP架構所需的代碼,我須要多添加90個文件,即便是複製粘貼這些代碼當時也耗費了我將近2個多小時的時間(固然不只僅是複製,還包括文件名,方法名稱的修改等等所需的細節)。固然,這也是促使我開源出KCommon這個使用Kotlin編寫的,基於MVP架構的極速開發框架的主要緣由。前端

KCommon能夠解決的開發中的痛點

  • 頁面狀態的切換,包括正常、加載失敗、加載中、空頁面和自定義的頁面
  • 對下拉刷新和上拉加載更多的邏輯作了完善的處理,極大的減小了開發者的工做量
  • 對網絡請求框架作了封裝,便於方便的請求網絡和加載緩存
  • 對網絡請求返回的錯誤進行了封裝,方便根據錯誤碼進行錯誤處理
  • 提供了自動生成MVP相關文件的模板代碼,實現了真正一鍵建立MVP的全部代碼

集成方法

  • api 'com.blackflagbin:kcommonlibrary:1.0.1'
  • 在根目錄的gradle文件中添加:
allprojects {
    repositories {
        //添加這一行依賴
        maven { url "https://jitpack.io" }
    }
}

複製代碼
  • 在自定義的Application類中的onCreate方法中初始化:
CommonLibrary.instance.initLibrary(this,
                BuildConfig.APP_URL,
                ApiService::class.java,
                CacheService::class.java,
                spName = "KCommonDemo",
                errorHandleMap = hashMapOf<Int, (exception: IApiException) -> Unit>(401 to { exception ->

                }, 402 to { exception ->

                }, 403 to { exception ->

                }),
                isDebug = BuildConfig.DEBUG)
複製代碼

詳細功能使用說明

若是隻是針對不一樣的模塊進行介紹的話,可能不是那麼容易理解,這裏我結合一個Kotlin編寫的Demo,來一步一步詳細演示如何使用這個極速開發框架。java

明確需求

首先咱們這個APP的需求很明確,要有統一的網絡錯誤處理、頁面的不一樣狀態切換、下拉刷新和上拉加載更多、處理網絡請求時的Loading效果、在無網絡時加載緩存數據,和使用MVP架構來編寫代碼。在這裏我使用Kotlin編寫整個APP的代碼,對Kotlin不熟悉的同窗也不用懼怕,Java和Kotlin的寫法基本是一致的,而且個人MVP模板文件也提供了Kotlin和Java兩個版本的選項。react

添加依賴,複製模板代碼

這兩步在集成方法中已經介紹過了。android

配置MultiDexEnable

因爲KCommon爲了方便開發依賴了不少開發中經常使用的第三方庫,完整的依賴以下所示:git

dependencies {
    api fileTree(include: ['*.jar'], dir: 'libs')
    api 'com.android.support:appcompat-v7:27.1.1'
    api 'com.android.support:recyclerview-v7:27.1.1'
    api 'org.jetbrains.anko:anko:0.10.3'
    api 'androidx.core:core-ktx:0.3'
    api 'com.android.support:multidex:1.0.3'
    api 'com.squareup.okhttp3:okhttp:3.10.0'
    api 'com.squareup.okhttp3:logging-interceptor:3.9.1'
    api 'com.squareup.retrofit2:retrofit:2.4.0'
    api 'com.squareup.retrofit2:converter-gson:2.4.0'
    api 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0'
    api 'io.reactivex.rxjava2:rxandroid:2.0.1'
    api 'com.github.VictorAlbertos.RxCache:runtime:1.8.3-2.x'
    api 'io.reactivex.rxjava2:rxjava:2.1.7'
    api 'com.github.VictorAlbertos.Jolyglot:gson:0.0.4'
    api 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
    api 'com.tbruyelle.rxpermissions2:rxpermissions:0.9.4@aar'
    api 'org.greenrobot:eventbus:3.0.0'
    api 'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.35'
    api 'com.github.Kennyc1012:MultiStateView:1.3.0'
    api 'com.github.ybq:Android-SpinKit:1.1.0'
    api 'com.blankj:utilcode:1.17.1'
    api 'com.github.bumptech.glide:glide:3.8.0'
    api 'com.github.anzaizai:EasySwipeMenuLayout:1.1.2'
    api 'com.trello.rxlifecycle2:rxlifecycle:2.2.1'
    api 'com.trello.rxlifecycle2:rxlifecycle-components:2.2.1'
    api 'com.trello.rxlifecycle2:rxlifecycle-kotlin:2.2.1'
    api 'com.trello.rxlifecycle2:rxlifecycle-android-lifecycle-kotlin:2.2.1'
    api 'org.jetbrains.kotlin:kotlin-stdlib:1.2.51'
    api 'com.android.support:cardview-v7:27.1.1'
    api 'com.hx.multi-image-selector:multi-image-selector:1.2.1'
    api 'com.android.support:design:27.1.1'
}
複製代碼

因此方法數基本上是要超過65535的,所以須要配置MultiDex:github

android {
    compileSdkVersion 27
    buildToolsVersion '27.0.3'
    defaultConfig {
        minSdkVersion 19
        targetSdkVersion 27
        versionCode 1
        versionName "1.0"
        //在這裏配置multiDex
        multiDexEnabled true
    }
}
複製代碼

建立項目的目錄結構

因爲開發中須要配合KCommonTemplate一鍵生成相關MVP代碼使用,因此對整個項目的目錄結構有着要求(若是項目目錄不正確的話,一鍵生成的模板代碼文件的位置會錯位)。後端

首先在項目的主包名下建立4個平級的package:appcommonmvpuiapi

根據名稱你們也很好理解,app 包中存放咱們自定義的Application,common 包中存放一些通用的基礎代碼,好比常量、數據類、網絡訪問接口類等等,mvp 包中存放咱們MVP架構所需的組件類,ui 包中存放咱們的Activity、Fragment和Adapter等等與界面相關的類。緩存

  • app 包中建立咱們自定義的Application,Application中的內容以後會詳細說明
  • common 包中建立 http 包,裏面建立兩個接口, ApiServiceCacheService 這兩個接口的名字是固定的,也是由於模板文件中寫死了這兩個接口的名字。使用過Retrofit的同窗應該都清楚,前者是存放網絡請求的接口,然後者是結合RxCache使用的存放緩存方法的接口。若是對RxCache不熟悉的同窗或者不須要緩存的同窗能夠把 CacheService 中的內容清空,保持一個空接口便可,可是 CacheService 這個接口文件必須存在。
  • mvp 包中分別建立 contractmodelpresenter 三個包,對MVP架構熟悉的同窗應該很是清楚這些,而這些包中的相關代碼會在咱們建立Activity或Fragment的時候一鍵生成。
  • ui 包中建立 activityfragment ,很好理解了,存放咱們開發中的Activity和fragment。

上面這些目錄結構都是在一個新的項目開發前必須建立好的。有的同窗可能看到個人Demo中在一些目錄中也添加了別的一些包,好比在 common 包下建立了 constantutilentity 等等包。其實除了以前提到的必須的目錄結構,你在Demo中看到的別的包都是可選的,這個隨你,只不過這些都是我我的的開發習慣。我習慣在 common 包下存放我項目中的常量、工具類、和數據實體類,同理,我也喜歡在 ui 包下存放adapter和自定義view。固然,這些都是經驗之談,我推薦你跟我採用相同的結構。咱們最後來看一張圖片有個更明確的概念。 bash

目錄結構

完成咱們自定義的Application類

class App : Application() {

    companion object {
        fun startLoginActivity(context: Context, loginClazz: Class<*>) {
            CommonLibrary.instance.headerMap = hashMapOf(
                    "token" to SPUtils.getInstance("KCommonDemo").getString("token", "123"))
            context.startActivity(
                    Intent(
                            context,
                            loginClazz).addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK))
        }
    }

    override fun attachBaseContext(base: Context) {
        super.attachBaseContext(base)
        MultiDex.install(this)

    }

    override fun onCreate() {
        super.onCreate()
        if (LeakCanary.isInAnalyzerProcess(this)) {
            // This process is dedicated to LeakCanary for heap analysis.
            // You should not init your app in this process.
            return;
        }
        LeakCanary.install(this)
        BlockCanary.install(this, AppBlockCanaryContext()).start()

        CommonLibrary.instance.initLibrary(this,
                BuildConfig.APP_URL,
                ApiService::class.java,
                CacheService::class.java,
                spName = "KCommonDemo",
                errorHandleMap = hashMapOf<Int, (exception: IApiException) -> Unit>(401 to { exception ->

                }, 402 to { exception ->

                }, 403 to { exception ->

                }),
                isDebug = BuildConfig.DEBUG)
    }
}
複製代碼

上面是Demo中自定義的Application,首先要重寫這個方法,處理Multidex:

//Multidex
    override fun attachBaseContext(base: Context) {
        super.attachBaseContext(base)
        MultiDex.install(this)
    }
複製代碼

而後再onCreate方法中初始化咱們的 CommonLibrary

CommonLibrary.instance.initLibrary(this,
                BuildConfig.APP_URL,
                ApiService::class.java,
                CacheService::class.java,
                spName = "KCommonDemo",
                errorHandleMap = hashMapOf<Int, (exception: IApiException) -> Unit>(401 to { exception ->

                }, 402 to { exception ->

                }, 403 to { exception ->

                }),
                isDebug = BuildConfig.DEBUG)
複製代碼

這裏傳入的第一個參數是Application自己,第二個參數是BaseUrl,以後是咱們以前提到的APIService和CacheService,以後傳入了一個spName,這個表示SharedPrefrence的文件名稱,以後是errorHandleMap,這存放了根據不一樣的網絡錯誤碼對應的回調,最後傳入一個isDebug表示debug環境下會開啓網絡日誌輸入,release環境下會關閉網絡日誌輸出。下面是詳細說明:

/**
     * 初始化
     * @param context Application
     * @param baseUrl retrofit所需的baseUrl
     * @param apiClass retrofit使用的ApisService::Class.java
     * @param cacheClass rxcache使用的CacheService::Class.java
     * @param spName Sharedpreference文件名稱
     * @param isDebug 是debug環境仍是release環境。debug環境有網絡請求的日誌,release反之
     * @param startPage 分頁列表的起始頁,有多是0,或者是2,這個看後臺
     * @param pageSize 分頁大小
     * @param headerMap 網絡請求頭的map集合,便於在網絡請求添加統一的請求頭,好比token之類
     * @param errorHandleMap 錯誤處理的map集合,便於針對相關網絡請求返回的錯誤碼來作相應的處理,好比錯誤碼401,token失效須要從新登陸
     * @param onPageCreateListener 對應頁面activity或fragment相關生命週期的回調,便於在頁面相關時機作一些統一處理,好比加入友盟統計須要在全部頁面的相關生命週期加入一些處理
     * @param onPageDestroyListener 對應頁面activity或fragment相關生命週期的回調,便於在頁面相關時機作一些統一處理,好比加入友盟統計須要在全部頁面的相關生命週期加入一些處理
     * @param onPageResumeListener 對應頁面activity或fragment相關生命週期的回調,便於在頁面相關時機作一些統一處理,好比加入友盟統計須要在全部頁面的相關生命週期加入一些處理
     * @param onPagePauseListener 對應頁面activity或fragment相關生命週期的回調,便於在頁面相關時機作一些統一處理,好比加入友盟統計須要在全部頁面的相關生命週期加入一些處理
     *
     */
    fun initLibrary(
            context: Application,
            baseUrl: String,
            apiClass: Class<*>,
            cacheClass: Class<*>,
            spName: String = "kcommon",
            isDebug: Boolean = true,
            startPage: Int = 1,
            pageSize: Int = 20,
            headerMap: Map<String, String>? = null,
            errorHandleMap: Map<Int, (exception: IApiException) -> Unit>? = null,
            onPageCreateListener: OnPageCreateListener? = null,
            onPageDestroyListener: OnPageDestroyListener? = null,
            onPageResumeListener: OnPageResumeListener? = null,
            onPagePauseListener: OnPagePauseListener? = null)
複製代碼

固然這些參數中前4個參數都是必須的,由於很明顯嘛,它們都沒有默認值。其他的參數若是有須要的話是能夠按需配置的。

若是是跟着Demo一塊兒看的話,有的小夥伴可能會對APP這段代碼中的:

companion object {
        fun startLoginActivity(context: Context, loginClazz: Class<*>) {
            CommonLibrary.instance.headerMap = hashMapOf(
                    "token" to SPUtils.getInstance("KCommonDemo").getString("token", "123"))
            context.startActivity(
                    Intent(
                            context,
                            loginClazz).addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK))
        }
    }
複製代碼

感到疑惑,這裏實際上是一個伴生對象,能夠理解爲Java中的靜態方法,主要是方便當token過時的時候跳轉到登陸頁面並將以前網絡請求中的token這個請求頭清空。固然,在咱們正在開發的這個Demo中是沒有用到的,這裏只是提供一種token過時,跳轉登陸頁的思路。

完成網絡數據類的編寫

common 包下建立 entity 數據類包,以後再 entity 包下建立 net 網絡數據類包,在 net 中建立 DataEntity 這個數據文件。

這個數據文件以下所示:

//最外層數據類
data class HttpResultEntity<T>(
        private var code: Int = 0,
        private var message: String = "",
        private var error: Boolean = false,
        private var results: T) : IHttpResultEntity<T> {
    override val isSuccess: Boolean
        get() = !error
    override val errorCode: Int
        get() = code
    override val errorMessage: String
        get() = message
    override val result: T
        get() = results
}


data class DataItem(
		@SerializedName("desc") var desc: String = "",
		@SerializedName("ganhuo_id") var ganhuoId: String = "",
		@SerializedName("publishedAt") var publishedAt: String = "",
		@SerializedName("readability") var readability: String = "",
		@SerializedName("type") var type: String = "",
		@SerializedName("url") var url: String = "",
		@SerializedName("who") var who: String = ""
)

複製代碼

我我的習慣將全部的數據類都寫到一個文件中,由於數據類都是很簡單的,所有寫在一個文件中看起來比較清晰,也便於管理。

DataEntity 中有兩個數據類,第一個是咱們網絡返回數據的最外層數據類,能夠看到,它實現了一個 KCommon 庫中定義的一個接口 IHttpResultEntity ,咱們來看一下這個接口:

interface IHttpResultEntity<T> {
    //網絡請求結果是否成功
    val isSuccess: Boolean

    //錯誤碼
    val errorCode: Int

    //錯誤信息
    val errorMessage: String

    //返回的有效數據
    val result: T
}
複製代碼

這個接口的意思其實很明顯了,註釋寫的很清楚。那麼有些還沒進入公司的小夥伴們可能會有問題了:爲何必需要添加一個實現了 IHttpResultEntity 接口的數據類呢?

咱們先來看一下公司開發中實際返回的數據結構:

{"data":null,"code":200,"message":null,"success":true}
複製代碼

能夠看到返回的數據包含4個部分,正好對應着接口中的4個部分。大多數的公司都會返回相似的數據結構,固然有些會字段名稱不同,也有一些會缺一些字段,這時候咱們應該靈活應變。

第二個 DataItemGankApi 返回的數據結構,比方說下面就是一條數據:

{
          "desc": "\u8fd8\u5728\u7528ListView\uff1f", 
          "ganhuo_id": "57334c9d67765903fb61c418", 
          "publishedAt": "2016-05-12T12:04:43.857000", 
          "readability": "", 
          "type": "Android", 
          "url": "http://www.jianshu.com/p/a92955be0a3e", 
          "who": "\u9648\u5b87\u660e"
        }
複製代碼

一鍵生成主頁面的MVP相關代碼

咱們日常寫MVP架構的代碼,雖然總體頁面邏輯看起來很是清晰:Model只管理數據的獲取、Presenter管理數據和頁面的交互邏輯、View只處理ui相關的事件。但這個清晰是有代價的,文章開篇已經提到過了:咱們要多寫3個文件 -> ContractModelPresenter 。對,,沒錯,一個Activity或者Fragment就要多寫三個文件,在我短短的開發生涯中,除此以外我還遇到過更過度的,說到這裏,可能有的小夥伴要跟我想到一塊去了:沒錯,就是 Dagger2 ,這個東西首先理解起來有些費勁,其次就是一個Activity或者Fragment也是要多配置2個文件: ComponentModule 。相信使用過 Dagger2 的朋友都懂我在說什麼,那麼問題來了,我只想寫一個頁面,但卻要多寫5個文件,這簡直不可忍受( Dagger2 我已經在新開發的項目中移除了,並且之後也不打算再使用,緣由嘛很簡單:使用很繁瑣,並且基本上沒什麼好的效果,本質上就是把new對象的代碼放在了別處。若是沒有用過 Dagger2 的同窗,請你聽我一句勸:珍惜生命,遠離 Dagger2 )。

那回到咱們的主題,咱們如今要建立一個主頁面。這個主頁面要有如下幾點功能:

  1. 使用MVP架構
  2. 包含頁面的加載、成功、失敗和空頁面的邏輯
  3. 這個頁面並不具備上拉加載更多的功能

要建立這樣一個頁面咱們有這麼幾個步驟:

  • 用鼠標選中項目的根包名,比方說Demo中的包名爲 kcommonproject ,記住,必定要是根包名(固然也不是說不能選中其餘包,只不過選中其餘包的話生成的相關代碼文件的位置會不太正確),以下圖所示:
  • Mac上是Command+N,Windows上是Ctrl+N,彈出新建文件的彈框,由於咱們要建立的是Activity,因此找到Activity的選項,進入以後能夠看到咱們的模板文件的選項由於咱們使用Kotlin開發,並且也沒不須要上拉加載更多的功能,因此咱們選擇 Kotlin MVP Activity 這個選項,點擊生成相關代碼,以下圖所示:

接下來就是見證奇蹟的時刻,你會發現你的mvp包中生成了相關MVP的代碼,而且在Activity中默認會有一些配置代碼,而且XML文件中也生成了便於切換頁面Loading、成功、失敗、空佈局的代碼,簡而言之,一鍵生成了你所需的一切。

接下來咱們來看一下這些生成的文件。

  • mvp 包下的 contract 包中生成了一個 MainContract 的接口:
interface MainContract {
    interface IMainModel

    interface IMainPresenter : IBasePresenter

    interface IMainView : IBaseView<Any?>
}
複製代碼

這是一個主頁面的 Contract 接口,包含了咱們 mvp 的三個接口,因爲咱們在 MainActivity 中並不作關於網絡請求相關的操做,因此咱們並無修改這個接口中的任何代碼。

  • mvp 包下的 model 包中生成了一個 MainModel 的實現類:
class MainModel : BaseModel<ApiService, CacheService>(), MainContract.IMainModel
複製代碼

能夠看到咱們的 MainModel 繼承了 BaseModel 而且實現了咱們 MainContract 中的 MainContract.IMainModel 。因爲並不涉及網絡數據的獲取,因此並無任何內容。

  • mvp 包下的 presenter 包中生成了一個 MainPresenter 的實現類:
class MainPresenter(iMainView: MainContract.IMainView) :
        BasePresenter<MainContract.IMainModel, MainContract.IMainView>(iMainView),
        MainContract.IMainPresenter {
    override val model: MainContract.IMainModel
        get() = MainModel()

    override fun initData(dataMap: Map<String, String>?) {
    }

}
複製代碼

能夠看到生成的 MainPresenter 繼承了 BasePresenter 而且實現了咱們 MainContract 中的 MainContract.IMainPresenter 。同時還持有了一個 MainModel 的引用對象 model ,這個主要是方便咱們在presenter中調用model中的方法獲取網絡數據。

還有一個 initData(dataMap: Map<String, String>?) 方法,這個方法從名稱也能夠理解,頁面加載數據的方法,須要傳入一個 Map 類型的參數。爲何要傳一個 Map 對象呢?主要緣由仍是在實際開發中咱們請求網絡接口所需的參數個數是不肯定的,可能不須要傳參數,比方說退出登陸的接口實際上是不須要傳參的;固然也可能傳個數不一樣的參數,比方說你有一個根據條件查詢數據的接口,然而這些條件並非必須的,能夠有,也能夠不傳,這個時候針對參數個數不一樣的問題,咱們須要一個數據結構來知足咱們的需求,而 Map 這個數據結構正好知足咱們的需求。

一樣的狀況,因爲在 MainActivity 中咱們並不處理網絡,因此代碼是不須要修改的。

  • ui 包下的 activity 包中生成了 MainActivity
class MainActivity : BaseActivity<ApiService, CacheService, MainPresenter, Any?>(),
        MainContract.IMainView {
    private val AVATAR_URL = "https://avatars2.githubusercontent.com/u/17843145?s=400&u=d417a5a50d47426c0f0b6b9ff64d626a36bf0955&v=4"
    private val ABOUT_ME_URL = "https://github.com/BlackFlagBin"
    private val READ_ME_URL = "https://github.com/BlackFlagBin/KCommonProject/blob/master/README.md"
    private val MORE_PROJECT_URL = "https://github.com/BlackFlagBin?tab=repositories"
    private val mTypeArray: Array<String> by lazy {
        arrayOf("all", "Android", "iOS", "休息視頻", "福利", "拓展資源", "前端", "瞎推薦", "App")
    }


    override val swipeRefreshView: SwipeRefreshLayout?
        get() = null

    override val multiStateView: MultiStateView?
        get() = null

    override val layoutResId: Int
        get() = R.layout.activity_main

    override val presenter: MainPresenter
        get() = MainPresenter(this)

    override fun initView() {
        super.initView()
        setupSlidingView()
        setupViewPager()
        rl_right.onClick {
            startActivity(
                    WebActivity::class.java, bundleOf("url" to ABOUT_ME_URL, "title" to "關於做者"))
        }
        ll_read_me.onClick {
            startActivity(
                    WebActivity::class.java, bundleOf("url" to READ_ME_URL, "title" to "ReadMe"))
        }
        ll_more_project.onClick {
            startActivity(
                    WebActivity::class.java, bundleOf("url" to MORE_PROJECT_URL, "title" to "更多項目"))
        }
        ll_clear_cache.onClick { clearCache() }


    }

    override fun initData() {
    }

    override fun showContentView(data: Any?) {
    }

    private fun setupSlidingView() {
        val slidingRootNav = SlidingRootNavBuilder(this).withToolbarMenuToggle(
                tb_main).withMenuOpened(
                false).withContentClickableWhenMenuOpened(false).withMenuLayout(
                R.layout.menu_main_drawer).inject()
        ll_menu_root.onClick { slidingRootNav.closeMenu(true) }
        Glide.with(this).load(
                AVATAR_URL).placeholder(
                R.mipmap.avatar).error(R.mipmap.avatar).dontAnimate().transform(
                GlideCircleTransform(
                        this)).into(iv_user_avatar)
    }

    private fun setupViewPager() {
        vp_content.adapter = MainPagerAdapter(supportFragmentManager)
        tl_type.setupWithViewPager(vp_content)
        vp_content.offscreenPageLimit = mTypeArray.size - 1
    }

    private fun clearCache() {
        val cache = CacheUtils.getInstance(cacheDir)
        val cacheSize = Formatter.formatFileSize(
                this, cache.cacheSize)
        cache.clear()
        toast("清除緩存$cacheSize")


    }

}
複製代碼

咱們須要關注的是帶 override 部分的成員和方法:

override val swipeRefreshView: SwipeRefreshLayout?
        get() = null
複製代碼

若是須要下拉刷新,須要在佈局XML文件中加入 SwipeRefreshView 並將這個View賦值給它。咱們的 MainActivity 不須要下拉刷新,因此默認是 null

override val multiStateView: MultiStateView?
        get() = null
複製代碼

這是負責頁面Loading、成功、失敗、空佈局切換的一個自定義View, 須要在佈局文件中加入 並賦值給它。額,實際上由於模板生成的佈局文件中會默認帶有 MultiStateView ,因此其實不須要咱們主動在佈局文件中加入。由於 MainActivity 並無網絡數據的加載,不須要切換頁面狀態,因此賦值爲 null

override val layoutResId: Int
        get() = R.layout.activity_main

    override val presenter: MainPresenter
        get() = MainPresenter(this)
複製代碼

這兩個放在一塊兒,前者是佈局文件的 id ,後者是咱們當前頁面的 presenter ,都是模板自動生成的,沒什麼可多說的。

override fun initView() {
        super.initView()
        setupSlidingView()
        setupViewPager()
        rl_right.onClick {
            startActivity(
                    WebActivity::class.java, bundleOf("url" to ABOUT_ME_URL, "title" to "關於做者"))
        }
        ll_read_me.onClick {
            startActivity(
                    WebActivity::class.java, bundleOf("url" to READ_ME_URL, "title" to "ReadMe"))
        }
        ll_more_project.onClick {
            startActivity(
                    WebActivity::class.java, bundleOf("url" to MORE_PROJECT_URL, "title" to "更多項目"))
        }
        ll_clear_cache.onClick { clearCache() }
    }
複製代碼

從名字能夠看出來,初始化界面佈局,全部關於頁面 不須要網絡數據 的UI的初始化代碼推薦放在這裏。

override fun initData() {
    }
複製代碼

很明顯了,在 initData 中咱們推薦的是加載網絡數據,一般會調用 mPresenter.initData(mDataMap) 來實現咱們網絡數據的加載。但 MainActivity 不須要加載網絡數據,因此咱們保持空置。

override fun showContentView(data: Any?) {
    }
複製代碼

這個方法的調用時機是在咱們請求網絡接口返回正確的數據以後,因此在這個方法中咱們能夠獲取所需的網絡數據,並修改UI。這裏一樣由於 MainActivity 不須要加載網絡數據,因此咱們保持空置。

  • layout 下的佈局文件 :
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <com.kennyc.view.MultiStateView
            android:id="@+id/multi_state_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            app:msv_emptyView="@layout/layout_empty"
            app:msv_errorView="@layout/layout_error"
            app:msv_loadingView="@layout/layout_loading"
            app:msv_viewState="content">


            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="@color/white"
                android:orientation="vertical">


                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:elevation="3dp"
                    android:orientation="vertical">

                    <android.support.v7.widget.Toolbar
                        android:id="@+id/tb_main"
                        android:layout_width="match_parent"
                        android:layout_height="48dp"
                        android:background="@android:color/transparent"
                        android:elevation="10dp">

                        <RelativeLayout
                            android:id="@+id/rl_right"
                            android:layout_width="50dp"
                            android:layout_height="match_parent"
                            android:layout_gravity="right"
                            android:gravity="center">

                            <ImageView
                                android:layout_width="20dp"
                                android:layout_height="20dp"
                                android:src="@mipmap/about_me"/>
                        </RelativeLayout>

                    </android.support.v7.widget.Toolbar>
                </LinearLayout>

                <android.support.design.widget.CoordinatorLayout
                    android:layout_width="match_parent"
                    android:layout_height="match_parent">

                    <android.support.design.widget.AppBarLayout
                        android:id="@+id/appbar_layout"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content">

                        <android.support.design.widget.CollapsingToolbarLayout
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:minHeight="0dp"
                            app:layout_scrollFlags="scroll|enterAlways|snap">

                            <android.support.design.widget.TabLayout
                                android:id="@+id/tl_type"
                                android:layout_width="match_parent"
                                android:layout_height="48dp"
                                android:layout_gravity="center_horizontal"
                                android:background="@color/white"
                                android:elevation="1dp"
                                app:layout_collapseMode="parallax"
                                app:layout_collapseParallaxMultiplier="0.1"
                                app:tabGravity="center"
                                app:tabIndicatorHeight="0dp"
                                app:tabMode="scrollable"
                                app:tabSelectedTextColor="@color/colorPrimary"
                                app:tabTextColor="@color/gray_text">
                            </android.support.design.widget.TabLayout>

                        </android.support.design.widget.CollapsingToolbarLayout>


                    </android.support.design.widget.AppBarLayout>


                    <android.support.v4.view.ViewPager
                        android:id="@+id/vp_content"
                        android:layout_width="match_parent"
                        android:layout_height="match_parent"
                        app:layout_behavior="@string/appbar_scrolling_view_behavior">

                    </android.support.v4.view.ViewPager>


                </android.support.design.widget.CoordinatorLayout>


            </LinearLayout>


        </com.kennyc.view.MultiStateView>
    </LinearLayout>

</android.support.design.widget.CoordinatorLayout>
複製代碼

值得注意的是 MultiStateView 中的這幾個屬性:

app:msv_emptyView="@layout/layout_empty"
            app:msv_errorView="@layout/layout_error"
            app:msv_loadingView="@layout/layout_loading"
            app:msv_viewState="content"
複製代碼

須要咱們傳入咱們本身編寫的空佈局、錯誤佈局、加載中佈局和當前頁面的狀態,固然你能夠直接使用我寫的默認佈局。頁面的狀態有4種 contentloadingerrorempty ,你須要當前頁面的初始狀態是什麼,就改變 msv_viewState 這個屬性值便可。通常來講若是須要加載網絡數據,初始狀態應該是 loading ,若是不須要加載網絡數據,就像咱們的 MainActivity 同樣的話,初始狀態應該是 content

一鍵生成 MainFragment 的MVP相關代碼

因爲這個App採用的是ViewPager+Fragment的UI結構,因此咱們還須要建立一個 MainPageFragment 。和建立 MainActivity 的時候幾乎是如出一轍的,惟一的區別在於以選擇一鍵生成的選項不是 Kotlin MVP Activity 而是 Kotlin RefreshAndLoadMore MVP Fragment ,以下圖所示:

由於 MainPageFragment 須要下拉刷新和上拉加載更多,因此咱們建立的是 Kotlin RefreshAndLoadMore MVP Fragment 而不是 Kotlin MVP Fragment 。這裏須要注意一點的是有的同窗會建立成 MainFragment ,結構發現fragment的MVP代碼覆蓋了 MainActivity 的MVP代碼,因此在建立Activity或者Fragment的時候應該避免兩者前面的名字重複,不然後者的MVP代碼會覆蓋前者。

和一鍵生成 MainActivity 時候同樣,這時候項目目錄中也會出現 MainPageContractMainPageModelMainPagePresenter 和咱們的 MainPageFragment 。跟以前的流程同樣,咱們來看一下這些生成的代碼:

  • MainPageContract
interface MainPageContract {
    interface IMainPageModel {
        fun getData(type: String, pageNo: Int, limit: Int): Observable<Optional<List<DataItem>>>
    }

    interface IMainPagePresenter : IBaseRefreshAndLoadMorePresenter

    interface IMainPageView : IBaseRefreshAndLoadMoreView<List<DataItem>>
}
複製代碼

跟以前的 MainContract 相似,區別在於咱們的頁面是帶 RefreshAndLoadMore 的,因此能夠看到 presenter 接口繼承了 IBaseRefreshAndLoadMorePresenter ,而 view 接口繼承了 IBaseRefreshAndLoadMoreView 。值得注意的是 ** IBaseRefreshAndLoadMoreView <List<DataItem>> ** 帶有一個泛型 ** List<DataItem> ** ,這個類型與咱們的 override fun showContentView(data: List<DataItem> ) 中參數的類型對應。

一樣要注意的點是在 IMainPageModel 這個接口中咱們定義了一個 getData(type: String, pageNo: Int, limit: Int) 的方法用於獲取分頁數據。要強調的是網絡接口實際返回的是 **List<DataItem> ** 類型的數據,但這個 getData 的返回值咱們須要在外面包上一層 Optional。這個 OptionalKCommon 庫中定義的一個類,其實很簡單:

/**
 * Created by blackflagbin on 2018/3/29.
 * 解決rxjava2不能處理null的問題,咱們把全部返回的有效數據包一層Optional,經過isEmpty判斷是否爲空
 */
data class Optional<T>(var data: T)  {

    fun isEmpty(): Boolean {
        return data == null
    }
}
複製代碼

這其實就是在網絡返回的原始數據上包了一層,那麼有的人會不理解了,爲何要包這一層,這不是畫蛇添足麼?

使用過 RxJava 的同窗應該很清楚,在咱們實際開發中,網絡接口間的順序調用是一個很常見的事情。比方說我在首頁想展現一個用戶當前小區下的通知公告,那其實確定要有兩個接口,獲取用戶當前小區的接口、根據小區id獲取通知公告的接口。這兩個接口之間存在着前後的邏輯關係,必須先拿到小區id,才能獲取通知公告。

一般來講,用過 RxJava 的同窗會使用 flatMap 這個操做符,即便是用戶沒有小區,接口返回的小區數據爲 null ,這在 RxJava1 的時候是不會存在任何問題的。然而,在新的 RxJava2 中,一旦接口返回的是 null ,而你又使用了 flatMap ,那麼很抱歉,程序會報錯,緣由就是在 RxJava2 中不支持 null 值事件的傳遞。

其實咱們可讓後臺不給咱們返回 null 來避免這個問題,但每每在實際開發中後臺爲了圖省事也是不會處理這種事情的,並且最多見的理由就是爲何IOS能夠返回 null,Android就不行?

因此爲了不跟後端的同窗過多的撕逼,咱們只能在返回的真實數據上包上一層 Optional ,來處理 RxJava2 中沒法傳遞 null 事件的問題。這也是我目前能夠想到的最好的處理方案,若是你們有更好的處理辦法,不妨經過留言告訴我,咱們共同窗習,共同進步。

  • MainPageModel
class MainPageModel : BaseModel<ApiService, CacheService>(), MainPageContract.IMainPageModel {
    override fun getData(type: String, pageNo: Int, limit: Int): Observable<Optional<List<DataItem>>> {
        return if (NetworkUtils.isConnected()) {
            mCacheService.getMainDataList(
                    mApiService.getMainDataList(
                            type, limit, pageNo).compose(DefaultTransformer()),
                    DynamicKeyGroup(type, pageNo),
                    EvictDynamicKeyGroup(true)).subscribeOn(Schedulers.io()).observeOn(
                    AndroidSchedulers.mainThread())
        } else {
            mCacheService.getMainDataList(
                    mApiService.getMainDataList(
                            type, limit, pageNo).compose(DefaultTransformer()),
                    DynamicKeyGroup(type, pageNo),
                    EvictDynamicKeyGroup(false)).subscribeOn(Schedulers.io()).observeOn(
                    AndroidSchedulers.mainThread())
        }
    }
}
複製代碼

在咱們的 MainPageModel 中,實現了 getData 這個在 IMainPageModel 中定義的接口方法。由於頁面的邏輯是網絡正常時獲取網絡數據,無網絡時加載緩存數據,因此方法中會有關於網絡狀態的判斷。咱們須要注意的是兩個變量: mApiServicemCacheService 。這兩個變量都是 BaseModel 中的成員變量,咱們在 BaseModel 的繼承類中均可以直接拿來使用。關於網絡請求和緩存我使用的是 RetrofitRxCache ,若是有不太瞭解的同窗能夠自行查看相關的文檔,我這裏就再也不贅述了。這裏會有小夥伴問:若是我不須要緩存怎麼辦?若是不須要緩存就更好辦了,這個方法直接返回

mApiService.getMainDataList(
                            type, limit, pageNo).compose(DefaultTransformer())
複製代碼

就能夠了。

有的同窗可能還會有疑問,爲何請求接口後面要加上 compose(DefaultTransformer()) 這麼一句,可不能夠省略?其實這一句是 KCommon 中網絡處理的關鍵部分,這句代碼對咱們網絡請求返回的結果作了相應的錯誤處理和 Optional 的包裝,因此這一句是必不可少的。對它的實現原理感興趣的同窗能夠直接經過 Android Studio 查看源碼,原理並不繁瑣。

  • MainPagePresenter
class MainPagePresenter(iMainPageView: MainPageContract.IMainPageView) :
        BasePresenter<MainPageContract.IMainPageModel, MainPageContract.IMainPageView>(iMainPageView),
        MainPageContract.IMainPagePresenter {
    override val model: MainPageContract.IMainPageModel
        get() = MainPageModel()

    override fun initData(dataMap: Map<String, String>?) {
        initData(dataMap, CommonLibrary.instance.startPage)
    }

    override fun initData(dataMap: Map<String, String>?, pageNo: Int) {
        if (!NetworkUtils.isConnected()) {
            mView.showTip("網絡已斷開,當前數據爲緩存數據")
        }
        if (pageNo == CommonLibrary.instance.startPage) {
            //若是請求的是分頁的首頁,必須先調用這個方法
            mView.beforeInitData()
            mModel.getData(
                    dataMap!!["type"].toString(),
                    pageNo,
                    CommonLibrary.instance.pageSize).bindToLifecycle(mLifecycleProvider).subscribeWith(
                    NoProgressObserver(mView, object : ObserverCallBack<Optional<List<DataItem>>> {
                        override fun onNext(t: Optional<List<DataItem>>) {
                            mView.showSuccessView(t.data)
                            mView.dismissLoading()
                        }

                        override fun onError(e: Throwable) {
                            mView.showErrorView("")
                            mView.dismissLoading()
                        }
                    }))
        } else {
            mModel.getData(
                    dataMap!!["type"].toString(),
                    pageNo,
                    CommonLibrary.instance.pageSize).bindToLifecycle(mLifecycleProvider).subscribeWith(
                    NoProgressObserver(
                            mView, mIsLoadMore = true))
        }
    }
}
複製代碼

能夠看到和 MainPresenter 的結構大同小異,區別在於多了 override fun initData(dataMap: Map<String, String>?, pageNo: Int) 這個方法,很明顯這是加載分頁用的。要注意我這個方法中代碼的寫法,須要注意的有這麼幾個點:

mView.beforeInitData() 在請求分頁的首頁時,必須先調用這行代碼進行數據的整理。以後再去使用 mModel 中的方法請求網絡。

.bindToLifecycle(mLifecycleProvider) 這句的目的是將網絡請求和當前頁面(Activity或Fragment)的生命週期綁定,當頁面結束的時候會終止網絡請求,防止內存泄漏,使用的是 RxLifeCycle ,有興趣的同窗能夠自行研究。個人建議是全部的 presenter 中的網絡請求調用中都要加上這一句,防止內存泄漏。

NoProgressObserver 因爲整個網絡框架使用的是 Rxjava+Retrofit+OKHttp ,因此最終是須要一個 Observer 來最終處理咱們網絡請求返回的數據。在 KCommon 中內置了兩種 ObserverNoProgressObserverProgressObserver 。從名字也能夠明白,分別是沒有加載動畫的和有加載動畫的 Observer 。那麼兩者的使用時機分別是什麼呢?簡而言之就是當你請求一個網絡時頁面須要有Loading動畫的顯示時使用 NoProgressObserver ,請求網絡時不須要Loading動畫時使用 ProgressObserver

mIsLoadMore = true 這是 NoProgressObserverProgressObserver 中都存在的一個默認參數,默認爲 false ,意思是 當前網絡請求是不是加載更多的請求 。當咱們加載非首頁的時候將之置爲 true

mView.showSuccessView(t.data) 當網絡請求成功時必須調用。做用是將當前頁面從Loading狀態切換到成功的狀態。

mView.showErrorView("網絡鏈接異常") 當網絡請求失敗時必須調用。做用是將當前頁面從Loading狀態切換到失敗的狀態。傳入的參數咱們能夠自定義錯誤的緣由,這個隨便寫。

mView.dismissLoading() 這個主要是帶有下拉刷新的頁面中當獲取到網絡數據(不管成功或者失敗)後,必須調用。

  • MainPageFragment
@SuppressLint("ValidFragment")
class MainPageFragment() :
        BaseRefreshAndLoadMoreFragment<ApiService, CacheService, MainPageContract.IMainPagePresenter, List<DataItem>>(),
        MainPageContract.IMainPageView {
    private val mTypeArray: Array<String> by lazy {
        arrayOf("all", "Android", "iOS", "休息視頻", "福利", "拓展資源", "前端", "瞎推薦", "App")
    }

    private lateinit var mType: String

    override val adapter: BaseQuickAdapter<*, *>?
        get() = MainPageAdapter(arrayListOf())

    override val recyclerView: RecyclerView?
        get() = rv_list

    override val layoutManager: RecyclerView.LayoutManager?
        get() = FixedLinearLayoutManager(activity)

    override val swipeRefreshView: SwipeRefreshLayout?
        get() = swipe_refresh

    override val multiStateView: MultiStateView?
        get() = multi_state_view

    override val layoutResId: Int
        get() = R.layout.fragment_main_page

    override val presenter: MainPageContract.IMainPagePresenter
        get() = MainPagePresenter(this)

    constructor(position: Int) : this() {
        mType = mTypeArray[position]
    }

    override fun initData() {
        mDataMap["type"] = mType
        mPresenter.initData(mDataMap)
    }

    override fun showContentView(data: List<DataItem>) {
        mAdapter?.onItemClickListener = BaseQuickAdapter.OnItemClickListener { adapter, view, position ->
            startActivity(
                    WebActivity::class.java,
                    bundleOf(
                            "url" to (mAdapter?.data!![position] as DataItem).url,
                            "title" to (mAdapter?.data!![position] as DataItem).desc))
        }
    }
}
複製代碼

能夠看到,和 MainActivity 類似處不少,我這裏着重說明一下不一樣的地方:

override val adapter: BaseQuickAdapter<*, *>?
        get() = MainPageAdapter(arrayListOf())
複製代碼

由於要有上拉加載更多的列表,因此很明顯須要一個 AdapterKCommon 中依賴了 BaseRecyclerViewAdapterHelper 這個第三方庫,我日常用起來挺方便的,並且功能很強大,在這裏也推薦你們使用。這個三方庫的具體使用方法我就不贅述了,你們能夠自行去 GitHub 上查看它的文檔。

override val recyclerView: RecyclerView?
        get() = rv_list
複製代碼

不用多說了吧,須要一個 RecyclerView 對象。

override val layoutManager: RecyclerView.LayoutManager?
        get() = FixedLinearLayoutManager(activity)
複製代碼

你們確定都知道要使用 LayoutManager ,但這裏必須使用我在 KCommon 中定義的幾個帶 Fixed 打頭的 LayoutManager ,這樣會避免一些詭異的異常。

override fun initData() {
        mDataMap["type"] = mType
        mPresenter.initData(mDataMap)
    }
複製代碼

initData 中咱們首先將類型參數存放進了 mDataMap 中,而後調用了 mPresenter.initData(mDataMap) 進行了網絡請求。

沒錯,只須要配置這麼幾個參數,咱們的一個帶有下拉刷新和上拉加載更多的頁面就完成了。相信作過相似功能頁面的同窗應該很清楚要實現一樣的功能若是所有本身寫的話會很繁瑣,但使用了 KCommon ,一切都會變得很是容易。

額外的提醒

到此爲止,經過一個簡單Demo的講解, KCommon 基礎的用法已經所有介紹完畢了,除了我上面說的以外,在 KCommon 中還集成依賴了一些我我的在實際項目開發中常常用到的,並且很是好用的第三方庫,這裏你們有興趣的話能夠嘗試瞭解一下。

最後要說的是我會長期維護和改進 KCommon ,若是你們在使用的過程當中存在疑惑,能夠在 GitHub 上提出 issue ,我會一一解答。感謝你們花時間看這麼一篇文章,若是個人努力解決了你們實際開發中的問題,提升了你們的效率,但願能夠順手給個 star ,謝謝。

GitHub地址

相關文章
相關標籤/搜索