Search code examples
androidviewmodelandroid-architecture-componentsandroid-viewmodelandroid-mvvm

Good practice for communicate between viewModel and fragment


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.


Solution

  • 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.