androidcallbackfirebase-authenticationandroid-livedataandroid-architecture

How to use livedata in an MVVM architecture


TLDR: How could I properly implement an MVVM architecture with LiveData?

I have a fragment class that observe a viewModel exposed livedata:

viewModel.loginResultLiveData.observe

class LoginFragment : Fragment() {

    private lateinit var binding: FragmentLoginBinding

    private val viewModel by fragmentScopedViewModel { injector.loginViewModel }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        binding = FragmentLoginBinding.inflate(inflater, container, false)

        val username = binding.loginInputField.toString()
        val password = binding.passwordInputField.toString()

        binding.loginSignInButton.setOnClickListener {  viewModel.login(
            username,
            password
        ) }

        viewModel.loginResultLiveData.observe(viewLifecycleOwner){
            when(it){
                is LoginResult.Success -> doSmth()
            }
        }

        return binding.root
    }
}

View model class simply ask for a mapped livedata object.

class LoginViewModel @Inject internal constructor(
    private val loginUseCase: LoginUseCase
) : ViewModel() {
    lateinit var loginResultLiveData: MutableLiveData<LoginResult>

    fun login(username: String, password: String) {
        loginResultLiveData = loginUseCase.login(username, password)
    }
}

Model uses use case, to map a result from the original format and also would map errors:

class LoginUseCase @Inject internal constructor(
    private val authRepository: AuthEmailRepository
) {
    var loginResultLiveData = MutableLiveData<LoginResult>()

    fun login(userName: String, password: String): MutableLiveData<LoginResult> {
        authRepository.login(userName, password)
            .addOnCompleteListener {
                if (it.isSuccessful) {
                    loginResultLiveData.postValue(LoginResult.Success)
                } else {
                    loginResultLiveData.postValue(LoginResult.Fail(it.exception.toString()))
                }
            }
        return loginResultLiveData
    }
}

The problem is that only after loginSignInButtonis clicked, the model creates a liveData object. But I'm starting to observe this object immediately after onClickListener is set. Also each time a button is clicked, this would create a new instance of viewModel.loginResultLiveData, which doesn’t make sense.

  binding.loginSignInButton.setOnClickListener {  viewModel.login(
        username,
        password
    ) }

    viewModel.loginResultLiveData.observe(viewLifecycleOwner){
        when(it){
            is LoginResult.Success -> doSmth()
        }
    }

How could I properly implement MVVM architecture with LiveData in this case?

I could also move logic I now have in LoginUseCaseto ModelView and then have something like this, which avoids the problem described before. But then I cannot delegate mapping/error handling to use case.

class LoginViewModel @Inject internal constructor(
    private val loginUseCase: LoginUseCase
) : ViewModel() {
    val loginResult: MutableLiveData<LoginResult> = MutableLiveData()

    fun login(username: String, password: String) = loginUseCase.login(username, password)
        .addOnCompleteListener {
            if (it.isSuccessful) {
                loginResult.postValue(LoginResult.Success)
            } else {
                loginResult.postValue(LoginResult.Fail(it.exception.toString()))
            }
        }
}

Solution

  • You are trying to observe a mutable LiveData that is only initialized after the onClickListener so you won't get it to work, also you have a lateinit property that is only initialized if you call the login method which will throw an exception.

    To solve your problem you can have a MediatorLiveData that will observe your other live data and pass the result back to your fragment observer.

    You can try the following:

    class LoginViewModel @Inject internal constructor(
        private val loginUseCase: LoginUseCase
    ) : ViewModel() {
    
        private var _loginResultLiveData = MediatorLiveData<LoginResult>()
        val loginResultLiveData: LiveData<LoginResult> = _loginResultLiveData
    
        fun login(username: String, password: String) {
            val loginUseCaseLiveData = loginUseCase.login(username, password)
            _loginResultLiveData.addSource(loginUseCaseLiveData) {
                _loginResultLiveData.value = it
            }
        }
    }