I'm implementing the viewModel and for communicate between the viewModel and fragment I'm doing this :
public class SplashViewModel extends AndroidViewModel {
private LiveData<Boolean> actions;
public SplashViewModel(@NonNull Application application) {
super(application);
actions= new MutableLiveData<>();
}
public void aViewModelMethod() {
//doing some stuff
if (stuff == X){
//I need to hide a view for exemple, I'm doing this
actions.postValue(true);
}
}
Now inside my Fragment I have an observable who is trigger when actions.postValue(true)
is reached
viewModel.actions.observe(getViewLifecycleOwner(), new Observer<Boolean>() {
@Override
public void onChanged(Boolean action) {
if (action){
databinding.myView.setVisibility(View.VISIBLE);
}
}
});
This work fine but if I have a lot of communication I need to implement each time a new variable, and observe it ? It's ok when they are 4 or 5 but what I am suppose to do when they are hundreds ?
I try to change boolean by an integer with a switch and a list of actions, but when the viewModel is initialize it's possible that several postValue are trigger and when I created the observable I'm only get the last one, that make sense.
Usually, I have two observable live data in my view model. First is represent the state of the whole screen. Second I use for "single-shot" events like toasts, navigation, showing dialogs.
My view model:
class PinCreateViewModel(...) : ViewModel() {
val event = MutableLiveData<BaseEvent<String>>()
val state = MutableLiveData<PinCreateViewState>()
}
I have a single state object for the whole screen:
sealed class PinCreateViewState {
object FirstInput : PinCreateViewState()
data class SecondInput(val firstEnteredPin: String) : PinCreateViewState()
object Error : PinCreateViewState()
object Loading : PinCreateViewState()
}
I think with this approach it's easy to think about my screen states, easy to design my screen as a finite state machine, and easy to debug. Especially, I like this approach to very complex screens. In this case, I have a single source of truth for my whole screen state.
But sometimes I want to show dialogs, toast or open new screens. These things are not part of my screen state. And this is why I want to handle them separately. And in this case, I use Events:
sealed class BaseEvent(private val content: String) {
var hasBeenHandled = false
private set
fun getContentIfNotHandled(): String? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
fun peekContent(): String = content
}
class ErrorEvent(content: String) : BaseEvent(content)
class MessageEvent(content: String) : BaseEvent(content)
And my Fragment interaction with ViewModel looks like this:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
observe(viewModel.event, this::onEvent)
observe(viewModel.state, this::render)
}
private fun render(state: PinCreateViewState) {
when (state) {
PinCreateViewState.FirstInput -> setFirstInputState()
is PinCreateViewState.SecondInput -> setSecondInputState()
PinCreateViewState.Error -> setErrorState()
PinCreateViewState.Loading -> setLoadingState()
}
}
fun onEvent(event: BaseEvent<String>) {
event.getContentIfNotHandled()?.let { text ->
when (event) {
is MessageEvent -> showMessage(text)
is ErrorEvent -> showError(text)
}
}
}
I really like Kotlin Sealed classes because it forces me to handle all possible cases. And I can find unhandled states even before compilation.