preface

After the Google Android team announced the Jetpack viewmodel, the MVVM architecture has become one of the most popular architectures for Android development. As shown below:

However, in Google’s previous official documentation, the Repository layer uses LiveData directly, and Even Jetpack Room supports LiveData, so the interface can return LiveData directly. So for a long time, various open source MVVM frameworks or blogs also use LiveData directly in the Repository layer.

Here we have a question: why does the Repository layer use LiveData? (Because LiveData should be associated with UI components such as Acvtivity and Fragment as documented by the official Lifecycle Lifecycle is very strange to put in the Repository layer).

So what’s the right thing to do? The following will demonstrate the MVVM framework based on the LiveData practice, its drawbacks, and the MVVM framework based on the Flow practice, and then solve the problems of LiveData by introducing Flow.

The MVVM practice of using LiveData at the Repository layer

Let’s first break down the MVVM framework to look at the data types and data flows across each tier:

Then go further and look at the design details:

1. Data processing process:

  1. Network data is obtained through the base network library (similar to LibNetwork, which is generally a business wrapper for Retrofit)

  2. RepositoryData

    > to LiveData

    > in the Repository layer.

    • Network request
    • A local request, usually a Room database
    • Network request + local request: local data is displayed first and the page is refreshed after the network data is successfully requested
  3. Transfix the data to the VO data that the UI layer understands, i.e. LiveData

    >

  4. Listen for changes to LiveData data at the UI level

2. Usage

The following is an example of requesting network data:

The UI layer:

    private val dailyMottoViewModel by viewModels<DailyMottoViewModel>()

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        binding = CommunicationsampleActivityLaunchTargetBinding.inflate(layoutInflater)
        setContentView(binding.root)

        dailyMottoViewModel.dailyMottoLiveData.observe(this) {
            when {
                it.isLoading() -> {
                    // show loading ui
                }
                it.isSuccess() -> {
                    // show success ui
                }
                it.isError() -> {
                    // show error ui
                }
            }
        }
        dailyMottoViewModel.requestDailyMotto()
    }
Copy the code

The ViewModel layer:

class DailyMottoViewModel : BaseViewModel() {

    private val dailyMottoRepository by lazyRepository<DailyMottoRepository>()

    private val dailyMottoMutableLiveData = MutableLiveData<RepositoryData<DailyMottoVO>>()
    val dailyMottoLiveData
        get() = dailyMottoMutableLiveData

    fun requestDailyMotto(a) {
        dailyMottoMutableLiveData.observeData(
            dataSource = dailyMottoRepository.getDailyMotto(),
            transformer = {
                it.transformToVO()
            }
        )
    }
}
Copy the code

The Repository layer:

class DailyMottoRepository : BaseRepository() {

    fun getDailyMotto(a): LiveData<RepositoryData<DailyMottoModel>> {
        return fetchNetworkData {
            WebServiceFactory.instance.fetchDailyMotto()
        }
    }
}
Copy the code

3. Implementation principle of Repository layer

    fun <T> fetchNetworkData(requestFun: suspend() - >WebResponse<T>, saveToLocal: ((T) - >Unit)? = null): LiveData<RepositoryData<T>> {
        val liveData = MutableLiveData<RepositoryData<T>>()
        repositoryScope.launch {
            / / Loading condition
            liveData.postValue(RepositoryData.loading())

            val result = invokeFunction(requestFun)
            if (result.isSuccessful()) {
                // Whether the data needs to be stored locally, usually in the Room databasesaveToLocal? .let { saveDataInLocal -> withContext(Dispatchers.IO) { result.data? .let { saveDataInLocal.invoke(it) } } }// Data request successful
                liveData.postValue(RepositoryData.success(result.data))}else {
                // Data request failed
                liveData.postValue(RepositoryData.error(RepositoryData.MSG_SERVER_ERROR, statusCode = result.code))

                // Handle common request exceptions, such as token invalidation and authentication
                if (RepositoryData.isSpecificErrorToken(result.code)) {
                    onTokenError(result.code)
                }
            }
        }
        return liveData
    }

    private suspend fun <T> invokeFunction(function: suspend() - >WebResponse<T>): WebResponse<T> = withContext(Dispatchers.IO) {
        val response: WebResponse<T> =
            try {
                function.invoke()
            } catch (ex: Exception) {
                XLog.e(TAG, "invokeFunction: ${ex.message}")
                WebResponse(code = -1)
            }
        response
    }
Copy the code

Disadvantages of using LiveData in Repository

The LiveData API is designed to be too simple to handle the many complex data-processing scenarios that might occur at the Repository layer. It is mainly reflected in the following three aspects:

  1. Thread switching is not supported
  2. Back pressure treatment is not supported
  3. Heavily dependent Lifecycle

Thread switching is not supported

In complex business scenarios, data is often processed multiple times with thread switching, similar to observeOn of RxJava and flowOn of Flow, while LiveData does not have such capability. So thread switching can only be done through coroutines, and at the Repository layer you can only customize repositoryScope and handle the logic of coroutine cancellation. That is:

    private var repositoryScope: CoroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
        get() {
            if(field.coroutineContext[Job]? .isActive ==true) {
                return field
            }
            val newScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
            repositoryScope = newScope
            return newScope
        }

    fun cancel(a) {
        repositoryScope.cancel()
    }
Copy the code

Back pressure treatment is not supported

LiveData shoulder the ability to provide data subscription for UI, so its data subscription can only be in the main thread. Although it can publish data through postValue in the child thread, postValue is called too fast in a short time, because there is no back pressure processing, only the latest data is retained. This may cause unexpected data loss problems.

Flow, on the other hand, has a complete back-pressure strategy that can handle complex data scenarios at the Repository layer.

Heavily dependent Lifecycle

LiveData relies on Lifecycle, has Lifecycle awareness, follows the Lifecycle of entities such as activities and fragments, and is used in non-UI scenarios where Lifecycle needs to be customized. Or use LiveData#observerForever (risk of leak). In the above case, the ViewModel needs to listen to The LiveData of the Repositoy layer, so it has to do something special to avoid memory leaks. Such as:

    private val tempLiveDataList = mutableMapOf<LiveData<*>, Observer<*>>()

    /** * dataSource -> [transformer] -> LiveData */
    fun <T, D> MutableLiveData<RepositoryData<T>>.observeData(
        dataSource: LiveData<RepositoryData<D>>,
        transformer: (D) - >T
    ) {
        val data = MediatorLiveData<RepositoryData<T>>()
        data.addSource(dataSource) {
            when {
                it.isError() -> checkPostErrorValue(it, transformer)
                it.isLoading() -> checkPostLoadingValue(it, transformer)
                else -> value = if (it.data= =null) RepositoryData.success(it.data) else
                    RepositoryData.success(transformer.invoke(it.data))}}data.observeForever(Observer<RepositoryData<T>> { }.apply { tempLiveDataList[data] = this})}/** * Clear the Observer in onCleared to avoid disclosure */
    override fun onCleared(a) {
        super.onCleared()
        tempLiveDataList.forEach {
            it.key.removeObserver(it.value as Observer<in Any>)
        }
    }
Copy the code

MVVM practices for using Flow at the Repository layer

Using Flow to replace the use of LiveData in the Repository layer mainly involves modifying the ViewModel layer and the Repository base class, and the modified logic is more concise and easy to read. And the official documentation has been updated, limit the use of LiveData scene, see: developer.android.com/topic/libra… :

It may be tempting to work LiveData objects in your data layer class, but LiveDatais not designed to handle asynchronous streams of data. Even though you can use LiveData transformations and MediatorLiveData to achieve this, this approach has drawbacks: the capability to combine streams of data is very limited and all LiveData objects (including ones created through transformations) are observed on the main thread. The code below is an example of how holding a LiveData in the Repository can block the main thread: If you need to use streams of data in other layers of your app, consider using Kotlin Flows and then converting them to LiveData in the ViewModel using asLiveData(). Learn more about using Kotlin Flow with LiveData in this codelab. For codebases built with Java, consider using Executors in conjuction with callbacks or RxJava.

1. Implementation principle of Repository layer

    protected fun <T> fetchNetworkData(saveToLocal: ((T) - >Unit)? = null, requestFun: suspend() - >WebResponse<T>): Flow<RepositoryData<T>> {
        return flow<RepositoryData<T>> {
            / / Loading condition
            emit(RepositoryData.loading())

            val webResponse = requestFun.invoke()
            if (webResponse.isSuccessful()) {
                // Whether the data needs to be stored locally, usually in the Room database
                webResponse.data? .let { saveToLocal? .invoke(it) }// Data request successful
                emit(RepositoryData.success(webResponse.data))}else {
                // Handle common request exceptions, such as token invalidation and authentication
                if (RepositoryData.isSpecificErrorToken(webResponse.code)) {
                    onTokenError(webResponse.code)
                }
                // Data request failed
                emit(RepositoryData.error(webResponse.msg, webResponse.data, webResponse.code))
            }

        }.flowOnIOWithCatch()
    }

    private fun <T> Flow<RepositoryData<T>>.flowOnIOWithCatch(a): Flow<RepositoryData<T>> {
        return this.catch {
            emit(RepositoryData.error("local data error with catch"))
        }.flowOn(Dispatchers.IO)
    }
Copy the code

2. Implementation principle of ViewModel layer

    /**
     * dataSource -> [transformer] -> LiveData
     */
    fun <T, D> MutableLiveData<RepositoryData<T>>.observeData(
        dataSource: Flow<RepositoryData<D>>,
        transformer: (D) -> T
    ) {
        dataSource.collectInLaunch {
            when {
                it.isError() -> checkPostErrorValue(it, transformer)
                it.isLoading() -> checkPostLoadingValue(it, transformer)
                else -> value = if (it.data == null) RepositoryData.success(it.data) else
                    RepositoryData.success(transformer.invoke(it.data))
            }
        }
    }

    private inline fun <T> Flow<T>.collectInLaunch(crossinline action: suspend (value: T) -> Unit) = viewModelScope.launch {
        collect {
            action.invoke(it)
        }
    }
Copy the code

3. Usage

Because the API design is consistent, the usage method does not change from before, so the switch can be seamless. The only change is to change the return data type of the Repository layer from LiveData to Flow:

class DailyMottoRepository : BaseRepository() {

    fun getDailyMotto(a): Flow<RepositoryData<DailyMottoModel>> {
        return fetchNetworkData {
            WebServiceFactory.instance.getDailyMotto()
        }
    }
}
Copy the code

conclusion

In summary, Flow can be used in the Repository layer to obtain data, and Retrofit and Room have their own Flow extension support, which is basically seamless. The ViewModel layer collects the Flow from the Repository layer, performs data conversion, transfers the Model to VO, and uses LiveData for UI update.

Since Flow is so good that it can replace the use of LiveData in the Repository layer, should we also use Flow in the ViewModel layer and say good bye to LiveData altogether? I’ll leave that to you