1. 程式人生 > 其它 >Kotlin 類委託(一):如何把一個列表頁優化到十幾行程式碼

Kotlin 類委託(一):如何把一個列表頁優化到十幾行程式碼

技術標籤:javaandroid安卓javascript移動開發

  • 相關文章

Kotlin 類委託(一):如何把一個列表頁優化到十幾行程式碼

Kotlin 類委託(二):實現原理及注意事項

痛點

​ 在之前,有用 玩AndroidAPI 寫了一個 Demo 專案 SampleProject,初期開發完成之後開始著手進行優化,就突然發現 首頁、專案、體系等 文章列表 資料結構相同、功能也相同,但是由於不同介面獲取資料的介面不同,導致同樣的程式碼寫了很多遍,一個介面程式碼少說百來行,這樣的重複低效肯定是不行的,必須要優化!

​ 這裡貼上原有 ViewModel 程式碼:

/** 公眾號文章列表 ViewModel,使用 [repository] 獲取相關資料,進行網路請求 */
class BjnewsArticlesViewModel(
        private val repository: ArticlesRepository
) : BaseViewModel() {

    /** 公眾號 id */
    var bjnewsId = ""

    /** 頁碼 */
    private var pageNumber: MutableLiveData<Int> = MutableLiveData()

    /** 文章列表返回資料 */
    private val articleListResultData: LiveData<NetResult<ArticleListEntity>> = pageNumber.switchMap { pageNum ->
        getBjnewsArticles(pageNum)
    }

    /** 文章列表資料 */
    val articleListData: LiveData<ArrayList<ArticleEntity>> = articleListResultData.map { result ->
        disposeArticleListResult(result)
    }

    /** 跳轉 WebView 資料 */
    val jumpWebViewData = MutableLiveData<WebViewActivity.ActionModel>()

    /** 重新整理狀態 */
    val refreshing: MutableLiveData<SmartRefreshState> = MutableLiveData()

    /** 重新整理回撥 */
    val onRefresh: () -> Unit = {
        pageNumber.value = NET_PAGE_START
    }

    /** 載入更多狀態 */
    val loadMore: MutableLiveData<SmartRefreshState> = MutableLiveData()

    /** 載入更多回調 */
    val onLoadMore: () -> Unit = {
        pageNumber.value = pageNumber.value.orElse(NET_PAGE_START) + 1
    }

    /** 文章列表的 `viewModel` 物件 */
    val articleListViewModel: ArticleListViewModel = object : ArticleListViewModel {

        /** 文章列表條目點選 */
        override val onArticleItemClick: (ArticleEntity) -> Unit = { item ->
            // 跳轉 WebView 開啟
            jumpWebViewData.value = WebViewActivity.ActionModel(item.id.orEmpty(), item.title.orEmpty(), item.link.orEmpty())
        }

        /** 文章收藏點選 */
        override val onArticleCollectClick: (ArticleEntity) -> Unit = { item ->
            if (item.collected.get().condition) {
                // 已收藏,取消收藏
                item.collected.set(false)
                unCollect(item)
            } else {
                // 未收藏,收藏
                item.collected.set(true)
                collect(item)
            }
        }
    }

    /** 獲取公眾號文章列表 */
    private fun getBjnewsArticles(pageNum: Int): LiveData<NetResult<ArticleListEntity>> {
        val result = MutableLiveData<NetResult<ArticleListEntity>>()
        viewModelScope.launch {
            try {
                // 獲取文章列表資料
                result.value = repository.getBjnewsArticles(bjnewsId, pageNum)
            } catch (throwable: Throwable) {
                Logger.t("NET").e(throwable, "getBjnewsArticles")
                result.value = NetResult.fromThrowable(throwable)
            }
        }
        return result
    }

    /** 處理文章列表返回資料 [result],並返回文章列表 */
    private fun disposeArticleListResult(result: NetResult<ArticleListEntity>): ArrayList<ArticleEntity> {
        val refresh = pageNumber.value == NET_PAGE_START
        val smartControl = if (refresh) refreshing else loadMore
        return if (result.success()) {
            smartControl.value = SmartRefreshState(loading = false, success = true, noMore = result.data?.over.toBoolean())
            articleListData.value.copy(result.data?.datas, refresh)
        } else {
            smartControl.value = SmartRefreshState(loading = false, success = false)
            articleListData.value.orEmpty()
        }
    }

    /** 收藏文章[item] */
    private fun collect(item: ArticleEntity) {
        viewModelScope.launch {
            try {
                // 收藏
                val result = repository.collectArticleInside(item.id.orEmpty())
                if (!result.success()) {
                    // 收藏失敗,提示、回滾收藏狀態
                    snackbarData.value = SnackbarModel(result.errorMsg)
                    item.collected.set(false)
                }
            } catch (throwable: Throwable) {
                Logger.t("NET").e(throwable, "collect")
                // 收藏失敗,提示、回滾收藏狀態
                snackbarData.value = SnackbarModel(throwable.showMsg)
                item.collected.set(false)
            }
        }
    }

    /** 取消收藏文章[item] */
    private fun unCollect(item: ArticleEntity) {
        viewModelScope.launch {
            try {
                // 取消收藏
                val result = repository.unCollectArticleList(item.id.orEmpty())
                if (!result.success()) {
                    // 取消收藏失敗,提示、回滾收藏狀態
                    snackbarData.value = SnackbarModel(result.errorMsg)
                    item.collected.set(true)
                }
            } catch (throwable: Throwable) {
                Logger.t("NET").e(throwable, "unCollect")
                // 取消收藏失敗,提示、回滾收藏狀態
                snackbarData.value = SnackbarModel(throwable.showMsg)
                item.collected.set(true)
            }
        }
    }
}

​ 上面的程式碼裡,有很多元素都是重複的,比如 文章列表資料、重新整理狀態、收藏、取消收藏、文章點選事件等。

如何進行優化

​ 根據上面已有的條件,我們能很容易就看出一個方案,就是將公用邏輯抽取成基類,讓各個列表介面繼承,這就有了第一套優化方案。

方案一:抽取基類

​ 只需要將程式碼中的重複元素抽取出來,封裝到基類裡面,將有差異的方法抽象暴露出來,子類各自實現不就可以了嗎?話不多說,直接上程式碼:

/** 文章列表 ViewModel 基類 */
abstract class BaseArticlesListViewModel(
    private val repository: ArticlesRepository
): BaseViewModel() {

    /** 頁碼 */
    private var pageNumber: MutableLiveData<Int> = MutableLiveData()

    /** 文章列表返回資料 */
    private val articleListResultData: LiveData<NetResult<ArticleListEntity>> = pageNumber.switchMap { pageNum ->
        getBjnewsArticles(pageNum)
    }

    /** 文章列表資料 */
    val articleListData: LiveData<ArrayList<ArticleEntity>> = articleListResultData.map { result ->
        disposeArticleListResult(result)
    }

    /** 跳轉 WebView 資料 */
    val jumpWebViewData = MutableLiveData<WebViewActivity.ActionModel>()

    /** 重新整理狀態 */
    val refreshing: MutableLiveData<SmartRefreshState> = MutableLiveData()

    /** 重新整理回撥 */
    val onRefresh: () -> Unit = {
        pageNumber.value = NET_PAGE_START
    }

    /** 載入更多狀態 */
    val loadMore: MutableLiveData<SmartRefreshState> = MutableLiveData()

    /** 載入更多回調 */
    val onLoadMore: () -> Unit = {
        pageNumber.value = pageNumber.value.orElse(NET_PAGE_START) + 1
    }

    /** 文章列表的 `viewModel` 物件 */
    val articleListViewModel: ArticleListViewModel = object : ArticleListViewModel {

        /** 文章列表條目點選 */
        override val onArticleItemClick: (ArticleEntity) -> Unit = { item ->
            // 跳轉 WebView 開啟
            jumpWebViewData.value = WebViewActivity.ActionModel(item.id.orEmpty(), item.title.orEmpty(), item.link.orEmpty())
        }

        /** 文章收藏點選 */
        override val onArticleCollectClick: (ArticleEntity) -> Unit = { item ->
            if (item.collected.get().condition) {
                // 已收藏,取消收藏
                item.collected.set(false)
                unCollect(item)
            } else {
                // 未收藏,收藏
                item.collected.set(true)
                collect(item)
            }
        }
    }

    /** 獲取公眾號文章列表 */
    private fun getBjnewsArticles(pageNum: Int): LiveData<NetResult<ArticleListEntity>> {
        val result = MutableLiveData<NetResult<ArticleListEntity>>()
        viewModelScope.launch {
            try {
                // 獲取文章列表資料
                result.value = loadArticlesList(pageNum)
            } catch (throwable: Throwable) {
                Logger.t("NET").e(throwable, "getBjnewsArticles")
                result.value = NetResult.fromThrowable(throwable)
            }
        }
        return result
    }

    /** 處理文章列表返回資料 [result],並返回文章列表 */
    private fun disposeArticleListResult(result: NetResult<ArticleListEntity>): ArrayList<ArticleEntity> {
        val refresh = pageNumber.value == NET_PAGE_START
        val smartControl = if (refresh) refreshing else loadMore
        return if (result.success()) {
            smartControl.value = SmartRefreshState(loading = false, success = true, noMore = result.data?.over.toBoolean())
            articleListData.value.copy(result.data?.datas, refresh)
        } else {
            smartControl.value = SmartRefreshState(loading = false, success = false)
            articleListData.value.orEmpty()
        }
    }

    /** 收藏文章[item] */
    private fun collect(item: ArticleEntity) {
        viewModelScope.launch {
            try {
                // 收藏
                val result = repository.collectArticleInside(item.id.orEmpty())
                if (!result.success()) {
                    // 收藏失敗,提示、回滾收藏狀態
                    snackbarData.value = SnackbarModel(result.errorMsg)
                    item.collected.set(false)
                }
            } catch (throwable: Throwable) {
                Logger.t("NET").e(throwable, "collect")
                // 收藏失敗,提示、回滾收藏狀態
                snackbarData.value = SnackbarModel(throwable.showMsg)
                item.collected.set(false)
            }
        }
    }

    /** 取消收藏文章[item] */
    private fun unCollect(item: ArticleEntity) {
        viewModelScope.launch {
            try {
                // 取消收藏
                val result = repository.unCollectArticleList(item.id.orEmpty())
                if (!result.success()) {
                    // 取消收藏失敗,提示、回滾收藏狀態
                    snackbarData.value = SnackbarModel(result.errorMsg)
                    item.collected.set(true)
                }
            } catch (throwable: Throwable) {
                Logger.t("NET").e(throwable, "unCollect")
                // 取消收藏失敗,提示、回滾收藏狀態
                snackbarData.value = SnackbarModel(throwable.showMsg)
                item.collected.set(true)
            }
        }
    }
    
    /** 抽象暴露方法,子類實現,獲取文章列表資料 */
    abstract suspend fun loadArticlesList(pageNum: Int): NetResult<ArticlesListEntity>
}

​ 在上面的基類基礎上,我們能很簡單的實現一個文章列表的 ViewModel

/** 公眾號文章列表 ViewModel,使用 [repository] 獲取相關資料,進行網路請求 */
class BjnewsArticlesViewModel(
        private val repository: ArticlesRepository
) : BaseArticlesListViewModel(repository) {

    /** 公眾號 id */
    var bjnewsId = ""
    
    override suspend fun loadArticlesList(pageNum: Int): NetResult<ArticlesListEntity> {
        return repository.getBjnewsArticles(bjnewsId, pageNum)
    }
    
}

​ 這麼一看已經達成了我標題的要求了,不是很簡單嗎?可是並不是所有介面都需要有收藏功能的,也並不是所有介面都需要做分頁載入的,如果把不同功能拆分成介面,按照需要組裝起來,即使是這樣也還是要封裝成好幾個不同情況的基類,更別說我也不想把 ViewModel 的繼承關係搞得太複雜,要是 能夠同時繼承多個類就好了

{% note info %}

沒錯,這裡就到了我們這篇文章的重點,達到類似 同時繼承多個類 的效果。

{% endnote %}

方案二:Kotlin 類委託

什麼是類委託?

委託模式 已經證明是實現繼承的一個很好的替代方式, 而 Kotlin 可以零樣板程式碼地原生支援它。具體說明可以參考Kotlin中文

​ 簡單來說,Kotlin 在語法層添加了對 委託模式 的支援,你可以簡單的通過 by 關鍵字來實現,我們來看實際案例。

​ 以超市中的水果為例,我們定義一個水果介面,裡面定義了獲取水果的名稱、外形、價格的方法

interface Fruit {
    /** 名稱 */
    fun name(): String
    /** 外形 */
    fun shape(): String
    /** 價格 */
    fun price(): String
}

​ 然後超市裡進了一批白心火龍果,我們定義一個類,繼承水果介面 Fruit

class WhitePitaya: Fruit {
    override fun name(): String {
        return "白心火龍果"
    }
    override fun shape(): String {
        return "火龍果的形狀"
    }
    override fun price(): String {
        return "12.8"
    }
}
val pitaya = WhitePitaya()
println("WhitePitaya={name=${pitaya.name()}, shape=${pitaya.shape()}, price=${pitaya.price()}}")
> WhitePitaya={name="白心火龍果", shape="火龍果的形狀", price="12.8"}

​ 接下來超市裡又來了一批紅心火龍果,按照習慣的方式,我們一般會定義一個類繼承 WhitePitaya,然後重寫 name()price() 方法,當然我們也可以用 類委託 的方式實現

class RedPitaya: Fruit by WhitePitaya {
    override fun name(): String {
        return "紅心火龍果"
    }
    override fun price(): String {
        return "22.8"
    }
}
val pitaya = RedPitaya()
println("RedPitaya={name=${pitaya.name()}, shape=${pitaya.shape()}, price=${pitaya.price()}}")
> RedPitaya={name="紅心火龍果", shape="火龍果的形狀", price="22.8"}

​ 這個時候列印 RedPitaya 的幾個方法,重寫的兩個方法已經變了,沒有重寫的方法列印的是 WhitePitaya 中的資料。可能有人要說了,這不就和繼承一個樣嗎,從這個例子上看,實現的效果確實和繼承一樣,但是我們都知道的是,一個類只能繼承一個類,但是能同時實現多個介面啊!!通過這種方式我們不就能實現類似繼承多個類的效果了嗎!

用類委託優化列表頁

​ 依照上面的思路,我們可以把列表頁的功能拆分為 獲取資料相關、收藏相關、文章點選相關 三個部分。

  1. 首先是獲取資料相關的介面:
/** 分頁獲取資料相關介面 */
interface ArticleListPagingInterface {
    
     /** 頁碼 */
    val pageNumber: MutableLiveData<Int>

    /** 文章列表資料 */
    val articleListData: LiveData<ArrayList<ArticleEntity>>

    /** 重新整理狀態 */
    val refreshing: MutableLiveData<SmartRefreshState>

    /** 載入更多狀態 */
    val loadMore: MutableLiveData<SmartRefreshState>

    /** 重新整理回撥 */
    val onRefresh: () -> Unit

    /** 載入更多回調 */
    val onLoadMore: () -> Unit

    /** 根據頁碼 [Int] 獲取文章列表資料 */
    var getArticleList: (Int) -> LiveData<NetResult<ArticleListEntity>>
}

/** 分頁獲取資料相關介面實現類 */
class ArticleListPagingInterfaceImpl
    : ArticleListPagingInterface {

    /** 頁碼 */
    override val pageNumber: MutableLiveData<Int> = MutableLiveData()

    /** 文章列表請求返回資料 */
    private val articleListResultData: LiveData<NetResult<ArticleListEntity>> = pageNumber.switchMap { pageNum ->
        getArticleList.invoke(pageNum)
    }

    /** 文章列表 */
    override val articleListData: LiveData<ArrayList<ArticleEntity>> = articleListResultData.switchMap { result ->
        disposeArticleListResult(result)
    }

    /** 重新整理狀態 */
    override val refreshing: MutableLiveData<SmartRefreshState> = MutableLiveData()

    /** 載入更多狀態 */
    override val loadMore: MutableLiveData<SmartRefreshState> = MutableLiveData()

    /** 重新整理回撥 */
    override val onRefresh: () -> Unit = {
        pageNumber.value = NET_PAGE_START
    }

    /** 載入更多回調 */
    override val onLoadMore: () -> Unit = {
        pageNumber.value = pageNumber.value.orElse(NET_PAGE_START) + 1
    }

    override var getArticleList: (Int) -> LiveData<NetResult<ArticleListEntity>> = {
        throw RuntimeException("Please set your custom method!")
    }

    /** 處理文章列表返回資料 [result],並返回文章列表 */
    private fun disposeArticleListResult(result: NetResult<ArticleListEntity>): LiveData<ArrayList<ArticleEntity>> {
        val liveData = MutableLiveData<ArrayList<ArticleEntity>>()
        val refresh = pageNumber.value == NET_PAGE_START
        val smartControl = if (refresh) refreshing else loadMore
        result.judge(
                onSuccess = {
                    smartControl.value = SmartRefreshState(loading = false, success = true, noMore = data?.over.toBoolean())
                    liveData.value = articleListData.value.copy(data?.datas, refresh)
                },
                onFailed = {
                    smartControl.value = SmartRefreshState(loading = false, success = false)
                    liveData.value = articleListData.value.orEmpty()
                },
                onFailed4Login = {
                    smartControl.value = SmartRefreshState(loading = false, success = false)
                    liveData.value = articleListData.value.orEmpty()
                    false
                }
        )
        return liveData
    }
}
  1. 收藏相關介面
/** 收藏相關介面 */
interface ArticleCollectionInterface {
    
     /** 收藏文章[item],使用 [snackbarData] 彈出提示 */
    suspend fun collect(item: ArticleEntity, snackbarData: MutableLiveData<SnackbarModel>)
    
    /** 取消收藏文章[item] */
    suspend fun unCollect(item: ArticleEntity, snackbarData: MutableLiveData<SnackbarModel>)
}

/** 收藏相關介面實現類 */
class ArticaleCollectionInterfaceImpl(
    private val repository: ArticleRepository
): ArticleCollectionInterface {
    
      /** 收藏文章[item],使用 [snackbarData] 彈出提示 */
    override suspend fun collect(item: ArticleEntity, snackbarData: MutableLiveData<SnackbarModel>) {
        try {
            // 收藏
            repository.collectArticleInside(item.id.orEmpty())
                    .judge(onFailed = {
                        // 收藏失敗,提示、回滾收藏狀態
                        snackbarData.value = this.toSnackbarModel()
                        item.collected.set(false)
                    })
        } catch (throwable: Throwable) {
            Logger.t("NET").e(throwable, "collect")
            // 收藏失敗,提示、回滾收藏狀態
            snackbarData.value = throwable.toSnackbarModel()
            item.collected.set(false)
        }
    }

    /** 取消收藏文章[item],使用 [snackbarData] 彈出提示 */
    override suspend fun unCollect(item: ArticleEntity, snackbarData: MutableLiveData<SnackbarModel>) {
        try {
            // 取消收藏
            repository.unCollectArticleList(item.id.orEmpty()).judge(onFailed = {
                // 取消收藏失敗,提示、回滾收藏狀態
                snackbarData.value = toSnackbarModel()
                item.collected.set(true)
            })
        } catch (throwable: Throwable) {
            Logger.t("NET").e(throwable, "unCollect")
            // 取消收藏失敗,提示、回滾收藏狀態
            snackbarData.value = throwable.toSnackbarModel()
            item.collected.set(true)
        }
    }
}
  1. 列表文章點選相關介面
/** 列表文章點選介面 */
interface ArticleListItemInterface {

    /** 文章列表條目點選 */
    val onArticleItemClick: (ArticleEntity) -> Unit

    /** 文章收藏點選 */
    val onArticleCollectClick: (ArticleEntity) -> Unit
}

/** 列表文章點選介面實現類 */
class ArticleListItemInterfaceImpl(
        private val viewModel: BaseViewModel,
        private val jumpToWebViewData: MutableLiveData<WebViewActivity.ActionModel>
) : ArticleListItemInterface {

    /** 文章列表條目點選 */
    override val onArticleItemClick: (ArticleEntity) -> Unit = { item ->
        jumpToWebViewData.value = WebViewActivity.ActionModel(item.id.orEmpty(), item.title.orEmpty(), item.link.orEmpty())
    }

    /** 文章收藏點選 */
    override val onArticleCollectClick: (ArticleEntity) -> Unit = fun(item) {
        val impl = viewModel as? ArticleCollectionInterface ?: return
        viewModel.viewModelScope.launch {
            if (item.collected.get().condition) {
                // 已收藏,取消收藏
                item.collected.set(false)
                impl.unCollect(item, viewModel.snackbarData)
            } else {
                // 未收藏,收藏
                item.collected.set(true)
                impl.collect(item, viewModel.snackbarData)
            }
        }
    }
}

​ 這樣我們對功能的拆分就完成了,接下來我們就來看看用 類委託 實現的列表頁是怎麼樣的吧

class BjnewsArticlesViewModel(
        private val repository: ArticleRepository
) : BaseViewModel(),
        ArticleCollectionInterface by ArticleCollectionInterfaceImpl(repository),
        ArticleListPagingInterface by ArticleListPagingInterfaceImpl() {

    /** 公眾號 id */
    var bjnewsId = ""

    init {
        getArticleList = { pageNum ->
            val result = MutableLiveData<NetResult<ArticleListEntity>>()
            viewModelScope.launch {
                try {
                    result.value = repository.getBjnewsArticles(bjnewsId, pageNum)
                } catch (throwable: Throwable) {
                    Logger.t("NET").e(throwable, "getArticleList")
                }
            }
            result
        }
    }

    /** 跳轉網頁資料 */
    val jumpWebViewData = MutableLiveData<WebViewActivity.ActionModel>()

    /** 列表事件 */
    val articleListItemInterface: ArticleListItemInterface by lazy {
        ArticleListItemInterfaceImpl(this, jumpWebViewData)
    }
}

​ 這就是優化之後的最終版本,不過好像有30多行、、、不過這並不重要( ̄y▽, ̄)╭ ,重要的是我們在這過程中使用 類委託 對功能的拆分,主要的功能邏輯都抽離到 ArticleCollectionInterfaceArticleListPagingInterface 中,並且實際使用了對應的 ArticleCollectionInterfaceImplArticleListPagingInterfaceImpl 中的實現。

總結

​ 經過上面的優化,我們減少了大量的重複程式碼,APP中的四五個相似的介面後能夠簡單的實現完成,當然,更重要的是不同的功能拆分出來後你就可以更具需求將不同的功能進行組裝,以達到不同的效果,並且功能分類清晰,讓專案更容易維護。

​ 那麼關於列表頁的優化我們就講到這裡了,下一章我們再來說說 Kotlin類委託 實現原理以及使用過程中需要注意的事項,可能有人也已經對我上面的部分程式碼產生疑問了,這點我們也會在下一章講解。

​ 想要我的原始碼嗎?想要的話可以全部給你,去找吧!我把所有原始碼都放在那裡!>> SampleProject <<

​ 感謝大家的耐心觀看,我是 WangJie0822 ,一個平平凡凡的程式猿,歡迎關注。

作者: WangJie0822
連結: http://www.wangjie0822.top/posts/c419796a/#%E6%80%BB%E7%BB%93
來源: WangJie0822
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。