How to make LiveData observe only once in Android [Kotlin]

Viewed 59

I have a situation where I want the livedata to be observed only once in the app. The problem is that I am working on the authentication for an app using some Node.js backend.

As I am sending the values to receive the response from the backend it's working fine till now. I observe that response and based on that I make changes to my fragment ( that is if the response received is true then move to next fragment, otherwise if it is false show a toast message ).

Now the problem is that :

Case 1: I opened the app, entered the right credentials and pressed the button, received true response from the server and goes to the next fragment.

Case 2: I opened the app, but entered the wrong credentials, I received a false from server and based on that the Toast is shown.

Case 3 (The issue): I opened the app, entered the wrong credentials and then without closing the fragment screen entered the right credentials by editing them, the app crashes and at the same time I receive multiple responses from the server via LiveData.

My observation: Looking more into that I found that the LiveData is attached to the fragment/activity and therefore it shows the last state. So as in case 3 the the last state was receiving the false value from backend it was used again and we were shown the error instead of going to the next screen.

Can anyone guide me how to solve this. Thanks

Some code that might be needed:

 binding.btnContinue.setOnClickListener {
            val number = binding.etMobileNumber.text.toString().toLong()
            Timber.d("Number: $number")
            authRiderViewModel.authDriver(number)
            checkNumber()
        }

Function which checks the number :

private fun checkNumber() {
        authRiderViewModel.response.observe(viewLifecycleOwner, Observer {
            Timber.d("Response: $it")
            if (it!!.success == true) {
                val action = LoginFragmentDirections.actionLoginFragmentToOtpFragment()
                findNavController().navigate(action)
                Timber.d("${it.message}")
            } else {
                Toast.makeText(requireContext(), "Number not registered", Toast.LENGTH_SHORT).show()
                binding.etMobileNumber.setText("")
            }
        })
    }

ViewModel code:


    private val _response = MutableLiveData<AuthResponse>()
    val response: LiveData<AuthResponse>
        get() = _response

    fun authDriver(number: Long) = viewModelScope.launch {
        Timber.d("Number: $number")
        myRepo.authDriver(number).let {
            _response.postValue(it)
        }
    }

P.S I have tried using something called SingleLiveEvent but it doesn't seem to work.

1 Answers

I would create a separate class that tracks the UI state you need and update it when the state is consumed. Something like the following. I don't really know what the parameter is for authDriver, so this is a more generic example.

sealed interface AuthState {
    object NotYetRequested: AuthState
    object AwaitingResponse: AuthState 
    class ResponseReceived(val response: AuthResponse): AuthState {
        var isHandled = false
            private set
        fun markHandled() {
            isHandled = true
        }
    }
}
// In ViewModel:

private val _authState = MutableLiveData<AuthState>().also {
    it.value = AuthState.NotYetRequested
}
val authState: LiveData<AuthState> get() = _authState

fun requestAuthentication() = viewModelScope.launch {
    _authState.value = AuthState.AwaitingResponse
    val response = myRepo.authenticate()
    _authState.value = AuthState.ResponseReceived(response)
}
// In Fragment:

viewModel.authState.observe(viewLifecycleOwner) { authState ->
    when (authState) {
        AuthState.NotYetRequested -> ShowUiRequestingAuthentication()
        AuthStateAwaitingResponse -> ShowIndeterminateProgressUi()
        is AuthStateResponseReceived -> when {
            authState.isHandled -> {} // do nothing? depends on your setup, might need to navigate to next screen if handled response is successful
            authState.response.isSuccessful -> {
                goToNextScreen()
                authState.markHandled()
            }
            else -> {
                showErrorToast()
                ShowUiRequestingAuthentication()
                authState.markHandled()
            }
        }
    }
}
Related