In the Jetpack architecture specification, the communication between the ViewModel and the View should follow a one-way data flow, with Events always flowing from the View to the VM and State from the VM to the View.
It is easy to break the one-way flow of data if the ViewModel does not expose the View to the right type of interface. There are two common examples of unreasonable interfaces:
- Exposed Mutable state
- Exposure Suspend method
Unreasonable 1: Expose Mutable state
ViewModel data states, whether LiveData or StateFlow, should be exposed using the Immutable interface type rather than the Mutable implementation. The View can only subscribe to these state changes in one direction, avoiding reverse state updates.
class MyViewModel: ViewModel() {
private val _loading = MutableLiveData<Boolean> ()val loading: LiveData<Boolean>
get() = _loading
}
Copy the code
To avoid exposing a Mutable type in the future, we need to do as above, defining the specific implementation of Loading as a private Mutable type for internal updating.
private val _loading : MutableStateFlow<Boolean? > = MutableStateFlow(null) val loading = _loading.asStateFlow()Copy the code
StateFlow is written similarly, but with asStateFlow you can write one less type declaration, but be careful not to use Custom Get (), otherwise asStateFlow will execute it multiple times. Declaring one more underlined private variable each time makes the code a bit cumbersome, which is why Issue wants Kotlin to add syntax such as the following to expose different types externally and internally.
//https://youtrack.jetbrains.com/issue/KT-14663
private val loading = MutableLiveData<Boolean> ()public get(): LiveData<Boolean>
Copy the code
In the absence of new syntax, one idea to keep code clean is to extract exposed abstract classes for the ViewModel:
abstract class MyViewModel: ViewModel() {
abstract val loading: LiveData<Boolean>}class MyViewModelImpl: MyViewModel() {
override val loading = MutableLiveData<Boolean> ()fun doSomeWork(a) {
// ...
loading.value = true}}Copy the code
As above, loading overwritten within MyViewModelImpl can be used as a Mutable type. While this adds an abstract class, it makes the code inside MyViewModelImpl much more concise, and hides more of the ViewModel implementation details from the outside, making it much more encapsulation. In order to create MyViewModel, you must use a custom Factory:
val vm : MyViewModel by viewModels { MyViewModelFactory() }
Copy the code
If your project introduces Hilt, you can use @bind to Bind the interface and implementation of the ViewModel. You don’t need to customize the Factory. You can just use by viewModels() as before
@Module
@InstallIn(ViewModelComponent::class)
abstract class MyViewModule {
@Binds
abstract fun MyViewModel(instance: MyViewModelImpl): MyViewModel
}
@HiltViewModel
class MyViewModelImpl @Inject constructor() : MyViewModel()
Copy the code
Irrationality 2: Expose the Suspend method
Exposing errors in the Suspend method is more common than exposing a Mutable state. According to the idea of one-way data flow, the ViewModel needs to provide an API to the View for sending Events. When defining the API, we need to avoid using the Suspend function for the following reasons:
- The data from the ViewModel should be obtained by subscriing to UiState, so the other methods of the ViewModel should have no return values, which are encouraged by the suspend function.
- In an ideal MVVM, the View’s job is simply to render the UI, and the business logic should be moved to the ViewModel to facilitate unit testing.
ViewModelScope
It can ensure the stable execution of some time-consuming tasks. If the View is exposed to suspend functions, the coroutine needs to be inlifecycleScope
In the horizontal and vertical screen scenarios, the task is interrupted.
Therefore, the API exposed by the ViewModel for the View should be non-suspended and cannot return a value. Here is an example of code from the official website:
// DO create coroutines in the ViewModel
class LatestNewsViewModel(
private val getLatestNewsWithAuthors: GetLatestNewsWithAuthorsUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow<LatestNewsUiState>(LatestNewsUiState.Loading)
val uiState: StateFlow<LatestNewsUiState> = _uiState
fun loadNews(a) {
viewModelScope.launch {
val latestNewsWithAuthors = getLatestNewsWithAuthors()
_uiState.value = LatestNewsUiState.Success(latestNewsWithAuthors)
}
}
}
// Prefer observable state rather than suspend functions from the ViewModel
class LatestNewsViewModel(
private val getLatestNewsWithAuthors: GetLatestNewsWithAuthorsUseCase
) : ViewModel() {
// DO NOT do this. News would probably need to be refreshed as well.
// Instead of exposing a single value with a suspend function, news should
// be exposed using a stream of data as in the code snippet above.
suspend fun loadNews(a) = getLatestNewsWithAuthors()
}
Copy the code
It is recommended in the code to expose a plain loadNews with no return value, and information about latestNewsWithAuthors should be obtained by subscriing to LatestNewsUiState.
A little bit confusing, the official document says:
Suspend functions in the ViewModel can be useful if instead of exposing state using a stream of data, Only a single value needs to be emitted. Developer.android.com/kotlin/coro…
Requests for single data are allowed to be returned using a suspend function. But I suggest you forget it for two reasons:
- Once the opening of the suspension function is open, it is easy to abuse regardless of the scene. If the overall data flow structure causes damage, it should be banned from the source
- In theory, there is no need for a single data request on the UI, and it can be translated into UiState through good design, which is more responsive to the programming model.
Read more
- One of the Seven deadly SINS of Jetpack MVVM: Taking Fragment as LifecycleOwner
- Jetpack MVVM’s second of the seven deadly SINS: starting coroutines in launchWhenX
- Jetpack MVVM seven deadly SINS: Loading data in onViewCreated
- Jetpack MVVM seven deadly SINS: Sending Events using LiveData/StateFlow
- Jetpack MVVM seven deadly SINS: Using LiveData in Repository