痛点
在之前,有用 玩Android 的 API 写了一个 Demo 项目 SampleProject,初期开发完成之后开始着手进行优化,就突然发现 首页、项目、体系等
文章列表 数据结构相同、功能也相同,但是由于不同界面获取数据的接口不同,导致同样的代码写了很多遍,一个界面代码少说百来行,这样的重复低效肯定是不行的,必须要优化!
这里贴上原有 ViewModel
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
| class BjnewsArticlesViewModel( private val repository: ArticlesRepository ) : BaseViewModel() {
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) }
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 }
val articleListViewModel: ArticleListViewModel = object : ArticleListViewModel {
override val onArticleItemClick: (ArticleEntity) -> Unit = { item -> 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 }
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() } }
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) } } }
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) } } } }
|
上面的代码里,有很多元素都是重复的,比如 文章列表数据、刷新状态、收藏、取消收藏、文章点击事件等。
如何进行优化
根据上面已有的条件,我们能很容易就看出一个方案,就是将公用逻辑抽取成基类,让各个列表界面继承,这就有了第一套优化方案。
方案一:抽取基类
只需要将代码中的重复元素抽取出来,封装到基类里面,将有差异的方法抽象暴露出来,子类各自实现不就可以了吗?话不多说,直接上代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
| 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) }
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 }
val articleListViewModel: ArticleListViewModel = object : ArticleListViewModel {
override val onArticleItemClick: (ArticleEntity) -> Unit = { item -> 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 }
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() } }
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) } } }
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
1 2 3 4 5 6 7 8 9 10 11 12 13
| class BjnewsArticlesViewModel( private val repository: ArticlesRepository ) : BaseArticlesListViewModel(repository) {
var bjnewsId = "" override suspend fun loadArticlesList(pageNum: Int): NetResult<ArticlesListEntity> { return repository.getBjnewsArticles(bjnewsId, pageNum) } }
|
这么一看已经达成了我标题的要求了,不是很简单吗?可是并不是所有界面都需要有收藏功能的,也并不是所有界面都需要做分页加载的,如果把不同功能拆分成接口,按照需要组装起来,即使是这样也还是要封装成好几个不同情况的基类,更别说我也不想把 ViewModel
的继承关系搞得太复杂,要是 能够同时继承多个类就好了!
没错,这里就到了我们这篇文章的重点,达到类似 同时继承多个类 的效果。
方案二:Kotlin 类委托
什么是类委托?
委托模式 已经证明是实现继承的一个很好的替代方式, 而 Kotlin 可以零样板代码地原生支持它。具体说明可以参考Kotlin中文。
简单来说,Kotlin 在语法层添加了对 委托模式 的支持,你可以简单的通过 by
关键字来实现,我们来看实际案例。
以超市中的水果为例,我们定义一个水果接口,里面定义了获取水果的名称、外形、价格的方法
1 2 3 4 5 6 7 8
| interface Fruit { fun name(): String fun shape(): String fun price(): String }
|
然后超市里进了一批白心火龙果,我们定义一个类,继承水果接口 Fruit
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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()
方法,当然我们也可以用 类委托 的方式实现
1 2 3 4 5 6 7 8 9 10 11
| 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
| 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
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!") }
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| interface ArticleCollectionInterface { suspend fun collect(item: ArticleEntity, snackbarData: MutableLiveData<SnackbarModel>) suspend fun unCollect(item: ArticleEntity, snackbarData: MutableLiveData<SnackbarModel>) }
class ArticaleCollectionInterfaceImpl( private val repository: ArticleRepository ): ArticleCollectionInterface { 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) } }
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| 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) } } } }
|
这样我们对功能的拆分就完成了,接下来我们就来看看用 类委托 实现的列表页是怎么样的吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| class BjnewsArticlesViewModel( private val repository: ArticleRepository ) : BaseViewModel(), ArticleCollectionInterface by ArticleCollectionInterfaceImpl(repository), ArticleListPagingInterface by ArticleListPagingInterfaceImpl() {
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▽, ̄)╭ ,重要的是我们在这过程中使用 类委托 对功能的拆分,主要的功能逻辑都抽离到 ArticleCollectionInterface
和 ArticleListPagingInterface
中,并且实际使用了对应的 ArticleCollectionInterfaceImpl
、ArticleListPagingInterfaceImpl
中的实现。
总结
经过上面的优化,我们减少了大量的重复代码,APP中的四五个相似的界面后能够简单的实现完成,当然,更重要的是不同的功能拆分出来后你就可以更具需求将不同的功能进行组装,以达到不同的效果,并且功能分类清晰,让项目更容易维护。
那么关于列表页的优化我们就讲到这里了,下一章我们再来说说 Kotlin类委托 实现原理以及使用过程中需要注意的事项,可能有人也已经对我上面的部分代码产生疑问了,这点我们也会在下一章讲解。
想要我的源码吗?想要的话可以全部给你,去找吧!我把所有源码都放在那里!>> SampleProject <<
感谢大家的耐心观看,我是 WangJie0822 ,一个平平凡凡的程序猿,欢迎关注。