• Related articles

Kotlin class delegate (1) : How to optimize a list page to a dozen lines of code

Kotlin class delegate (II) : Implementation principle and matters needing attention

Pain points

Before, I wrote a Demo project SampleProject using Android API. After the initial development was completed, I started to optimize it. Suddenly, I found that the data structure and functions of article lists such as homepage, project and system were the same. Lead to the same code to write many times, an interface code said less than a hundred lines, such a repeated inefficient is certainly not good, must be optimized!

Paste the original ViewModel code here:

/** * Public account article list ViewModel, use [repository] to retrieve relevant data, make web request */
class BjnewsArticlesViewModel(
        private val repository: ArticlesRepository
) : BaseViewModel() {

    /** public id */
    var bjnewsId = ""

    / * * * / page Numbers
    private var pageNumber: MutableLiveData<Int> = MutableLiveData()

    /** Article list returns data */
    private val articleListResultData: LiveData<NetResult<ArticleListEntity>> = pageNumber.switchMap { pageNum ->
        getBjnewsArticles(pageNum)
    }

    /** Article list data */
    val articleListData: LiveData<ArrayList<ArticleEntity>> = articleListResultData.map { result ->
        disposeArticleListResult(result)
    }

    /** Jump to WebView data */
    val jumpWebViewData = MutableLiveData<WebViewActivity.ActionModel>()

    /** Refresh the status */
    val refreshing: MutableLiveData<SmartRefreshState> = MutableLiveData()

    /** Refresh callback */
    val onRefresh: () -> Unit = {
        pageNumber.value = NET_PAGE_START
    }

    /** Load more states */
    val loadMore: MutableLiveData<SmartRefreshState> = MutableLiveData()

    /** Load more callbacks */
    val onLoadMore: () -> Unit = {
        pageNumber.value = pageNumber.value.orElse(NET_PAGE_START) + 1
    }

    /** The 'viewModel' object for the article list */
    val articleListViewModel: ArticleListViewModel = object : ArticleListViewModel {

        /** Article list entry click */
        override val onArticleItemClick: (ArticleEntity) -> Unit = { item ->
            // Jump WebView open
            jumpWebViewData.value = WebViewActivity.ActionModel(item.id.orEmpty(), item.title.orEmpty(), item.link.orEmpty())
        }

        /** click */
        override val onArticleCollectClick: (ArticleEntity) -> Unit = { item ->
            if (item.collected.get().condition) {
                // Unsubscribe
                item.collected.set(false)
                unCollect(item)
            } else {
                // Do not collect, collect
                item.collected.set(true)
                collect(item)
            }
        }
    }

    /** Get the list of articles */
    private fun getBjnewsArticles(pageNum: Int): LiveData<NetResult<ArticleListEntity>> {
        val result = MutableLiveData<NetResult<ArticleListEntity>>()
        viewModelScope.launch {
            try {
                // Get the article list data
                result.value = repository.getBjnewsArticles(bjnewsId, pageNum)
            } catch (throwable: Throwable) {
                Logger.t("NET").e(throwable, "getBjnewsArticles")
                result.value = NetResult.fromThrowable(throwable)
            }
        }
        return result
    }

    /** Returns data [result] and returns the list of articles */
    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()
        }
    }

    /** select [item] */
    private fun collect(item: ArticleEntity) {
        viewModelScope.launch {
            try {
                / / collection
                val result = repository.collectArticleInside(item.id.orEmpty())
                if(! result.success()) {// Roll back the state of the collection
                    snackbarData.value = SnackbarModel(result.errorMsg)
                    item.collected.set(false)}}catch (throwable: Throwable) {
                Logger.t("NET").e(throwable, "collect")
                // Roll back the state of the collection
                snackbarData.value = SnackbarModel(throwable.showMsg)
                item.collected.set(false)}}}/** Unbookmark [item] */
    private fun unCollect(item: ArticleEntity) {
        viewModelScope.launch {
            try {
                // Cancel the collection
                val result = repository.unCollectArticleList(item.id.orEmpty())
                if(! result.success()) {// Failed to cancel favorites, prompting and rolling back favorites
                    snackbarData.value = SnackbarModel(result.errorMsg)
                    item.collected.set(true)}}catch (throwable: Throwable) {
                Logger.t("NET").e(throwable, "unCollect")
                // Failed to cancel favorites, prompting and rolling back favorites
                snackbarData.value = SnackbarModel(throwable.showMsg)
                item.collected.set(true)}}}}Copy the code

In the above code, many elements are repeated, such as article list data, refresh status, favorites, unfavorites, article click events, etc.

How to optimize

Given the above conditions, it is easy to see a solution that extracts the common logic into a base class, which is inherited by the list interfaces. This is the first set of optimizations.

Scheme 1: Extract the base class

Just extract the duplicate elements from the code, encapsulate them in the base class, expose the different method abstractions, and implement them separately. Without further ado, go directly to the code:

/** Article list ViewModel base class */
abstract class BaseArticlesListViewModel(
    private val repository: ArticlesRepository
): BaseViewModel() {

    / * * * / page Numbers
    private var pageNumber: MutableLiveData<Int> = MutableLiveData()

    /** Article list returns data */
    private val articleListResultData: LiveData<NetResult<ArticleListEntity>> = pageNumber.switchMap { pageNum ->
        getBjnewsArticles(pageNum)
    }

    /** Article list data */
    val articleListData: LiveData<ArrayList<ArticleEntity>> = articleListResultData.map { result ->
        disposeArticleListResult(result)
    }

    /** Jump to WebView data */
    val jumpWebViewData = MutableLiveData<WebViewActivity.ActionModel>()

    /** Refresh the status */
    val refreshing: MutableLiveData<SmartRefreshState> = MutableLiveData()

    /** Refresh callback */
    val onRefresh: () -> Unit = {
        pageNumber.value = NET_PAGE_START
    }

    /** Load more states */
    val loadMore: MutableLiveData<SmartRefreshState> = MutableLiveData()

    /** Load more callbacks */
    val onLoadMore: () -> Unit = {
        pageNumber.value = pageNumber.value.orElse(NET_PAGE_START) + 1
    }

    /** The 'viewModel' object for the article list */
    val articleListViewModel: ArticleListViewModel = object : ArticleListViewModel {

        /** Article list entry click */
        override val onArticleItemClick: (ArticleEntity) -> Unit = { item ->
            // Jump WebView open
            jumpWebViewData.value = WebViewActivity.ActionModel(item.id.orEmpty(), item.title.orEmpty(), item.link.orEmpty())
        }

        /** click */
        override val onArticleCollectClick: (ArticleEntity) -> Unit = { item ->
            if (item.collected.get().condition) {
                // Unsubscribe
                item.collected.set(false)
                unCollect(item)
            } else {
                // Do not collect, collect
                item.collected.set(true)
                collect(item)
            }
        }
    }

    /** Get the list of articles */
    private fun getBjnewsArticles(pageNum: Int): LiveData<NetResult<ArticleListEntity>> {
        val result = MutableLiveData<NetResult<ArticleListEntity>>()
        viewModelScope.launch {
            try {
                // Get the article list data
                result.value = loadArticlesList(pageNum)
            } catch (throwable: Throwable) {
                Logger.t("NET").e(throwable, "getBjnewsArticles")
                result.value = NetResult.fromThrowable(throwable)
            }
        }
        return result
    }

    /** Returns data [result] and returns the list of articles */
    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()
        }
    }

    /** select [item] */
    private fun collect(item: ArticleEntity) {
        viewModelScope.launch {
            try {
                / / collection
                val result = repository.collectArticleInside(item.id.orEmpty())
                if(! result.success()) {// Roll back the state of the collection
                    snackbarData.value = SnackbarModel(result.errorMsg)
                    item.collected.set(false)}}catch (throwable: Throwable) {
                Logger.t("NET").e(throwable, "collect")
                // Roll back the state of the collection
                snackbarData.value = SnackbarModel(throwable.showMsg)
                item.collected.set(false)}}}/** Unbookmark [item] */
    private fun unCollect(item: ArticleEntity) {
        viewModelScope.launch {
            try {
                // Cancel the collection
                val result = repository.unCollectArticleList(item.id.orEmpty())
                if(! result.success()) {// Failed to cancel favorites, prompting and rolling back favorites
                    snackbarData.value = SnackbarModel(result.errorMsg)
                    item.collected.set(true)}}catch (throwable: Throwable) {
                Logger.t("NET").e(throwable, "unCollect")
                // Failed to cancel favorites, prompting and rolling back favorites
                snackbarData.value = SnackbarModel(throwable.showMsg)
                item.collected.set(true)}}}/** Abstract expose method, subclass implementation, get article list data */
    abstract suspend fun loadArticlesList(pageNum: Int): NetResult<ArticlesListEntity>
}
Copy the code

Based on the above base class, we can easily implement a ViewModel for a list of articles

/** * Public account article list ViewModel, use [repository] to retrieve relevant data, make web request */
class BjnewsArticlesViewModel(
        private val repository: ArticlesRepository
) : BaseArticlesListViewModel(repository) {

    /** public id */
    var bjnewsId = ""
    
    override suspend fun loadArticlesList(pageNum: Int): NetResult<ArticlesListEntity> {
        return repository.getBjnewsArticles(bjnewsId, pageNum)
    }
    
}
Copy the code

So a look has reached the requirements of my title, is not very simple? But not all interface will need to have the functions of collection, and not all interface will need to do paging loading, if the split into different function interface, according to the need to assemble it, even if it is still needs to be encapsulated into several different conditions of the base class, not to mention I don’t want to make the ViewModel inheritance too complicated, If only you could inherit more than one class at a time!

{% note info %}

That’s right, which brings us to the point of this article, which is to achieve the effect of inheriting multiple classes at once.

{% endnote %}

Scheme 2: Kotlin class delegate

What is a class delegate?

The delegate pattern has proven to be a good alternative to implementing inheritance, and Kotlin can support it natively with zero boilerplate code. For details, please refer to Kotlin in Chinese.

In a nutshell, Kotlin has added support for the delegate pattern in the syntax layer, which you can simply implement with the by keyword. Let’s look at a practical example.

Take the fruit in the supermarket as an example. We define a fruit interface that defines the method to get the name, shape, and price of the fruit

interface Fruit {
    / * * name * /
    fun name(a): String
    /** 外形 */
    fun shape(a): String
    / * * * / price
    fun price(a): String
}
Copy the code

And then we get a bunch of white heart pitaya in the supermarket, and we define a class that inherits Fruit interface

class WhitePitaya: Fruit {
    override fun name(a): String {
        return "White heart pitaya"
    }
    override fun shape(a): String {
        return "The shape of the dragon fruit."
    }
    override fun price(a): String {
        return "12.8"}}val pitaya = WhitePitaya()
println("WhitePitaya={name=${pitaya.name()}, shape=${pitaya.shape()}, price=${pitaya.price()}}")
> WhitePitaya={name="White heart pitaya", shape="The shape of the dragon fruit.", price="12.8"}
Copy the code

The next batch of red pitaya arrives in the supermarket. In the usual way, we would define a class that inherits WhitePitaya and override the name() and price() methods, but we could also use class delegates

class RedPitaya: Fruit by WhitePitaya {
    override fun name(a): String {
        return "Red pitaya"
    }
    override fun price(a): String {
        return "22.8"}}val pitaya = RedPitaya()
println("RedPitaya={name=${pitaya.name()}, shape=${pitaya.shape()}, price=${pitaya.price()}}")
> RedPitaya={name="Red pitaya", shape="The shape of the dragon fruit.", price="22.8"}
Copy the code

At this time, several methods of printing RedPitaya and the two methods of rewriting have been changed. The method without rewriting prints the data in WhitePitaya. Some people may say, it is not the same as inheritance, from this example, the effect of the implementation is indeed the same as inheritance, but we all know that ** a class can only inherit a class, but can simultaneously implement multiple interfaces ah!! ** In this way we can achieve the same effect as inheriting multiple classes!

Optimize the list page with class delegate

According to the above ideas, we can split the function of the list page into three parts: data acquisition related, favorites related, article click related.

  1. The first is to get the interface associated with the data:
/** ** ** /
interface ArticleListPagingInterface {
    
     / * * * / page Numbers
    val pageNumber: MutableLiveData<Int>

    /** Article list data */
    val articleListData: LiveData<ArrayList<ArticleEntity>>

    /** Refresh the status */
    val refreshing: MutableLiveData<SmartRefreshState>

    /** Load more states */
    val loadMore: MutableLiveData<SmartRefreshState>

    /** Refresh callback */
    val onRefresh: () -> Unit

    /** Load more callbacks */
    val onLoadMore: () -> Unit

    /** Get article list data based on page number [Int] */
    var getArticleList: (Int) -> LiveData<NetResult<ArticleListEntity>>
}

/ * * pages to get the data related to interface implementation class * /
class ArticleListPagingInterfaceImpl
    : ArticleListPagingInterface {

    / * * * / page Numbers
    override val pageNumber: MutableLiveData<Int> = MutableLiveData()

    /** Article list request returns data */
    private val articleListResultData: LiveData<NetResult<ArticleListEntity>> = pageNumber.switchMap { pageNum ->
        getArticleList.invoke(pageNum)
    }

    /** list of articles */
    override val articleListData: LiveData<ArrayList<ArticleEntity>> = articleListResultData.switchMap { result ->
        disposeArticleListResult(result)
    }

    /** Refresh the status */
    override val refreshing: MutableLiveData<SmartRefreshState> = MutableLiveData()

    /** Load more states */
    override val loadMore: MutableLiveData<SmartRefreshState> = MutableLiveData()

    /** Refresh callback */
    override val onRefresh: () -> Unit = {
        pageNumber.value = NET_PAGE_START
    }

    /** Load more callbacks */
    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!")}/** Returns data [result] and returns the list of articles */
    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
    }
}
Copy the code
  1. Collection related interface
/** Favorites interface */
interface ArticleCollectionInterface {
    
     /** Select [item], use [snackbarData] to pop up prompt */
    suspend fun collect(item: ArticleEntity, snackbarData: MutableLiveData<SnackbarModel>)
    
    /** Unbookmark [item] */
    suspend fun unCollect(item: ArticleEntity, snackbarData: MutableLiveData<SnackbarModel>)
}

/** Bookmark the interface implementation class */
class ArticaleCollectionInterfaceImpl(
    private val repository: ArticleRepository
): ArticleCollectionInterface {
    
      /** Select [item], use [snackbarData] to pop up prompt */
    override suspend fun collect(item: ArticleEntity, snackbarData: MutableLiveData<SnackbarModel>) {
        try {
            / / collection
            repository.collectArticleInside(item.id.orEmpty())
                    .judge(onFailed = {
                        // Roll back the state of the collection
                        snackbarData.value = this.toSnackbarModel()
                        item.collected.set(false)})}catch (throwable: Throwable) {
            Logger.t("NET").e(throwable, "collect")
            // Roll back the state of the collection
            snackbarData.value = throwable.toSnackbarModel()
            item.collected.set(false)}}/** Unbookmark article [item], use [snackbarData] popup prompt */
    override suspend fun unCollect(item: ArticleEntity, snackbarData: MutableLiveData<SnackbarModel>) {
        try {
            // Cancel the collection
            repository.unCollectArticleList(item.id.orEmpty()).judge(onFailed = {
                // Failed to cancel favorites, prompting and rolling back favorites
                snackbarData.value = toSnackbarModel()
                item.collected.set(true)})}catch (throwable: Throwable) {
            Logger.t("NET").e(throwable, "unCollect")
            // Failed to cancel favorites, prompting and rolling back favorites
            snackbarData.value = throwable.toSnackbarModel()
            item.collected.set(true)}}}Copy the code
  1. To list articles, click on the relevant interface
/** List article click interface */
interface ArticleListItemInterface {

    /** Article list entry click */
    val onArticleItemClick: (ArticleEntity) -> Unit

    /** click */
    val onArticleCollectClick: (ArticleEntity) -> Unit
}

/** list article click interface implementation class */
class ArticleListItemInterfaceImpl(
        private val viewModel: BaseViewModel,
        private val jumpToWebViewData: MutableLiveData<WebViewActivity.ActionModel>
) : ArticleListItemInterface {

    /** Article list entry click */
    override val onArticleItemClick: (ArticleEntity) -> Unit = { item ->
        jumpToWebViewData.value = WebViewActivity.ActionModel(item.id.orEmpty(), item.title.orEmpty(), item.link.orEmpty())
    }

    /** click */
    override val onArticleCollectClick: (ArticleEntity) -> Unit = fun(item) {
        val impl = viewModel as? ArticleCollectionInterface ? :return
        viewModel.viewModelScope.launch {
            if (item.collected.get().condition) {
                // Unsubscribe
                item.collected.set(false)
                impl.unCollect(item, viewModel.snackbarData)
            } else {
                // Do not collect, collect
                item.collected.set(true)
                impl.collect(item, viewModel.snackbarData)
            }
        }
    }
}
Copy the code

Now that we’ve split the functionality, let’s see what a list page looks like with a class delegate

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

    /** public 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
        }
    }

    /** Redirect page data */
    val jumpWebViewData = MutableLiveData<WebViewActivity.ActionModel>()

    /** List events */
    val articleListItemInterface: ArticleListItemInterface by lazy {
        ArticleListItemInterfaceImpl(this, jumpWebViewData)
    }
}
Copy the code

This is the final version after optimization, but it seems to have more than 30 lines, but that’s not important. ( ̄y▽, ̄)╭, the important thing is that we used the class delegate to split the function in this process. The function of the main logic is pulling away to ArticleCollectionInterface and ArticleListPagingInterface, And the actual use of the corresponding ArticleCollectionInterfaceImpl, ArticleListPagingInterfaceImpl implementation.

conclusion

Through the optimization of the above, we reduce a lot of duplicate code, after four or five similar interface in the APP to perform simple implementation, of course, more important is after the split of different function you can assemble more demand to different function, to achieve different effects, and functional classification is clear, make the project easier to maintain.

So that’s it for list page optimization. In the next chapter, we’ll talk about the Kotlin class delegate implementation and what to pay attention to when using it. Some of you may have questions about some of the code above, and we’ll cover that in the next chapter.

Want my source code? You can have it all if you want. Go get it! I put all the source code there! >> SampleProject <<

Thank you for your patience to watch, I am WangJie0822, an ordinary program ape, welcome to pay attention.

Author: WangJie0822 links: www.wangjie0822.top/posts/c4197… Copyright belongs to the author. Commercial reprint please contact the author for authorization, non-commercial reprint please indicate the source.