Kotlin 类委托(一):如何把一个列表页优化到十几行代码

  • 相关文章

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
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值