Use Kotlin delegates to split more complex ViewModels

Requirement Background

  1. In an actual development scenario, the data of a page may be composed of data of multiple businesses.
  2. Implemented using the MVVM architecture, storing and processing multiple business data in the ViewModel, and notifying the View layer to refresh the UI.

Traditional implementation

For example, in the above example, the page consists of 3 module data.

We can create a ViewModel and 3 LiveData to drive refresh the corresponding UI.

 class HomeViewModel() : ViewModel() {<!-- -->

        private val _newsViewState = MutableLiveData<String>()
        val newsViewState: LiveData<String>
            get() = _newsViewState

        private val _weatherState = MutableLiveData<String>()
        val weatherState: LiveData<String>
            get() = _weatherState

        private val _imageOfTheDayState = MutableLiveData<String>()
        val imageOfTheDayState: LiveData<String>
            get() = _imageOfTheDayState

fun getNews(){<!-- -->}
fun getWeather(){<!-- -->}
fun getImage(){<!-- -->}
\t\t\t\t
    }

This kind of implementation has a disadvantage, that is, as the business iterates, the logic of the page becomes complicated, and the ViewModel class code here will become complicated and bloated.

At this time, it may need to consider splitting the ViewModel.

One way to achieve it is to simply split it into three ViewModels, and each ViewModel handles the corresponding business. But this will bring other problems, that is, when using the View layer, it will be more troublesome to judge the current business and then obtain the corresponding ViewModel.

If you need kotlin study notes, please click here to get them for free

Optimization implementation

Target:

  • Split the ViewModel into multiple sub-ViewModels, each sub-ViewModel only focuses on processing its own business logic.
  • Try to consider the maintainability and scalability of the code

Kotlin delegate

  • Delegation (Delegate) is a language feature of Kotlin, which is used to implement the proxy mode more elegantly.
  • Essentially, after using the by syntax, the compiler will help generate relevant code.
  • Class delegation: The methods of a class are not defined in the class, but are directly delegated to another object for processing.
  • Both the base class and the delegated class implement the same interface. In the bytecode generated at compile time, the methods inherited from the Base interface will be delegated to BaseImpl for processing.
// basic interface
interface Base {<!-- -->
    fun print()
}

// base object
class BaseImpl(val x: Int) : Base {<!-- -->
    override fun print() {<!-- --> print(x) }
}

// delegated class
class Derived(b: Base) : Base by b

fun main(args: Array<String>) {<!-- -->
    val b = BaseImpl(10)
    Derived(b).print() // finally calls Base#print()
}

Concrete implementation

Define the interface of the child ViewModel, and the corresponding implementation class

 interface NewsViewModel {<!-- -->
        companion object {<!-- -->
            fun create(): NewsViewModel = NewsViewModelImpl()
        }

        val newsViewState: LiveData<String>

        fun getNews()
    }

    interface WeatherViewModel {<!-- -->
        companion object {<!-- -->
            fun create(): WeatherViewModel = WeatherViewModelImpl()
        }

        val weatherState: LiveData<String>

        fun getWeather()
    }

    interface ImageOfTheDayStateViewModel {<!-- -->
        companion object {<!-- -->
            fun create(): ImageOfTheDayStateViewModel = ImageOfTheDayStateImpl()
        }

        val imageState: LiveData<String>

        fun getImage()
    }

    class NewsViewModelImpl : NewsViewModel, ViewModel() {<!-- -->
        override val newsViewState = MutableLiveData<String>()

        override fun getNews() {<!-- -->
            newsViewState.postValue("Test")
        }
    }

    class WeatherViewModelImpl : WeatherViewModel, ViewModel() {<!-- -->
        override val weatherState = MutableLiveData<String>()

        override fun getWeather() {<!-- -->
            weatherState.postValue("Test")
        }
    }

    class ImageOfTheDayStateImpl : ImageOfTheDayStateViewModel, ViewModel() {<!-- -->
        override val imageState = MutableLiveData<String>()

        override fun getImage() {<!-- -->
            imageState.postValue("Test")
        }
    }

  • Divide a large module into several small business modules, which are processed by the corresponding ViewModel, keeping each other as independent as possible.
  • Define interface classes and provide fields and methods that need to be exposed externally
  • Define the interface implementation class, which is responsible for implementing the business details of the ViewModel internally, modifying the corresponding field values, and implementing the corresponding methods.
  • In this implementation, there is no need to declare an additional private variable with a dash every time, as in the above example. And it can hide more implementation details of the ViewModel from the outside, and the encapsulation is better.

Combined ViewModel

 interface HomeViewModel : NewsViewModel, WeatherViewModel, ImageOfTheDayStateViewModel {<!-- -->
        companion object {<!-- -->
            fun create(activity: FragmentActivity): HomeViewModel {<!-- -->
                return ViewModelProviders.of(activity, object : ViewModelProvider.Factory {<!-- -->
                    override fun <T : ViewModel?> create(modelClass: Class<T>): T {<!-- -->
                        return if (modelClass == HomeViewModelImpl::class.java) {<!-- -->
                            @Suppress("UNCHECKED_CAST")

                            val newsViewModel = NewsViewModel. create()
                            val weatherViewModel = WeatherViewModel. create()
                            val imageOfTheDayStateImpl = ImageOfTheDayStateViewModel. create()

                            HomeViewModelImpl(
                                newsViewModel,
                                weatherViewModel,
                                imageOfTheDayStateImpl
                            ) as T
                        } else {<!-- -->
                            modelClass. newInstance()
                        }

                    }
                }).get(HomeViewModelImpl::class.java)
            }
        }
    }

    class HomeViewModelImpl(
        private val newsViewModel: NewsViewModel,
        private val weatherViewModel: WeatherViewModel,
        private val imageOfTheDayState: ImageOfTheDayStateViewModel
    ) : ViewModel(),
        HomeViewModel,
        NewsViewModel by newsViewModel,
        WeatherViewModel by weatherViewModel,
        ImageOfTheDayStateViewModel by imageOfTheDayState {<!-- -->

        val subViewModels = listOf(newsViewModel, weatherViewModel, imageOfTheDayState)

        override fun onCleared() {<!-- -->
            subViewModels. filterIsInstance(BaseViewModel::class.java)
                .forEach {<!-- --> it.onCleared() }
            super. onCleared()
        }
    }

  • Define the interface class HomeViewModel, which inherits the interfaces of multiple child ViewModels
  • Define the implementation class HomeViewModelImpl, combine multiple child ViewModels, and handle the corresponding interface to the corresponding implementation class through the form of Kotlin class delegation.
  • In this way, you can split the business logic of the corresponding module into the corresponding child ViewModel for processing.
  • If you need to add a new business data in the future, you only need to add the ViewModel corresponding to the corresponding sub-module without modifying the ViewModel corresponding to other sub-modules.
  • Customize ViewModelFactory and provide a static method of create for external acquisition and creation of HomeViewModel.

How to use

  • For the View layer, you only need to get the HomeViewModel.
  • Call the exposed method, and finally delegate to the corresponding child ViewModel implementation class for processing.
 val viewModel = HomeViewModel.create(this)

        viewModel. getNews()
        viewModel. getWeather()
        viewModel. getImage()

        viewModel.newsViewState.observe(this) {<!-- -->

        }
        viewModel.weatherState.observe(this) {<!-- -->

        }
        viewModel.imageState.observe(this) {<!-- -->

        }

Extension

  • In the above example, under the HomeViewModel, it can be composed of several child ViewMdeols.
  • With business expansion, NewsViewModel, WeatherViewModel, and ImageOfTheDayStateViewModel may also be composed of several sub-ViewModels. It can also be realized by referring to the above method, and finally a “ViewModel tree” will be formed, and the ViewModel of each node is responsible for processing the corresponding business logic.

Summary

Here is just a way to split the ViewModel. If it is applied in the project, it can be modified as needed.