What is a possible issue with LiveData that doesn't return a value?

36 views Asked by At

I have a view with two fields: one for the email and another for the phone number. I need to validate them by updating a button on the view.

There is the code:

@HiltViewModel
class CreateAccountEnterEmailViewModel @Inject constructor(
    appData: AppData,
) : ViewModel() {
    private var email by mutableStateOf("")
    private var phoneNumber by mutableStateOf("")
    private var _isPhoneNumberFieldVisibleState: MutableStateFlow<Boolean> = MutableStateFlow(appData.isPhoneNumberRequiredForRegistrationParam.orDef())
    val isPhoneNumberFieldVisible: LiveData<Boolean> = _isPhoneNumberFieldVisibleState.asLiveData()

    class PairMediatorLiveData<F, S>(firstLiveData: LiveData<F>, secondLiveData: LiveData<S>) : MediatorLiveData<Pair<F?, S?>>() {
        init {
            addSource(firstLiveData) { firstLiveDataValue: F -> value = firstLiveDataValue to secondLiveData.value }
            addSource(secondLiveData) { secondLiveDataValue: S -> value = firstLiveData.value to secondLiveDataValue }
        }
    }

    private val isEmailValid: LiveData<Boolean> =
        snapshotFlow { email }
        .map { validateEmail(email = it) }
        .asLiveData()

    private val isPhoneValid: LiveData<Boolean> =
        snapshotFlow { phoneNumber }
            .map { validatePhone(phone = it) }
            .asLiveData()

    val isContinueBtnEnabled: LiveData<Boolean> = PairMediatorLiveData(isEmailValid, isPhoneValid)
        .switchMap {
            return@switchMap liveData {
                emit(it.first ?: false && it.second ?: false)
            }
        }

    fun updateEmail(email: String) {
        this.email = email
    }

    fun updatePhoneNumber(phoneNumber: String) {
        this.phoneNumber = phoneNumber
    }

The idea is that when the user updates their email or phone number, this update is intercepted by a mediator that provides the appropriate state depending on whether validation has passed or not.

However, for some reason, I see that nothing goes beyond calling the updateEmail or updatePhoneNumber method. What am I doing wrong?

P.S. There is, of course, a subscription in the view to isContinueBtnEnabled.

2

There are 2 answers

0
leopanic13 On

I took a look at your code and I can surely say that I'm not 100% sure what might be wrong with your logic. Just wanted to mention that.

I see that there is a subscription in the view to isContinueBtnEnabled but I believe ViewModel's logic for enabling the Continue Button might be flawed. I think the switchMap block is not evaluating as expected.

The incorrect implementation of the PairMediatorLiveData and switchMap transformations might be leading to incorrect logic for when we want to enable the Continue Button.

Maybe we can simplify the logic by directly observing changes in the email and phone number validity LiveData and updating the combined validity state accordingly instead of using a custom PairMediatorLiveData and switchMap.

@HiltViewModel
class CreateAccountEnterEmailViewModel @Inject constructor(
    appData: AppData,
) : ViewModel() {
    private var email by mutableStateOf("")
    private var phoneNumber by mutableStateOf("")
    private var _isPhoneNumberFieldVisibleState: MutableStateFlow<Boolean> = MutableStateFlow(appData.isPhoneNumberRequiredForRegistrationParam.orDef())
    val isPhoneNumberFieldVisible: LiveData<Boolean> = _isPhoneNumberFieldVisibleState.asLiveData()

    private val isEmailValid: LiveData<Boolean> =
        snapshotFlow { email }
            .map { validateEmail(email = it) }
            .asLiveData()

    private val isPhoneValid: LiveData<Boolean> =
        snapshotFlow { phoneNumber }
            .map { validatePhone(phone = it) }
            .asLiveData()

    val isContinueBtnEnabled: LiveData<Boolean> = MediatorLiveData<Boolean>().apply {
        addSource(isEmailValid) { emailValid -> // directly observe changes in the isEmailValid
            val phoneValid = isPhoneValid.value ?: false
            value = emailValid && phoneValid
        }
        addSource(isPhoneValid) { phoneValid -> // directly observe changes in the isPhoneValid
            val emailValid = isEmailValid.value ?: false
            value = emailValid && phoneValid
        }
    }

    fun updateEmail(email: String) {
        this.email = email
    }

    fun updatePhoneNumber(phoneNumber: String) {
        this.phoneNumber = phoneNumber
    }
}

I tried to simplify the logic and ensure that the combined validity state of email and phone number is correctly reflected in the isContinueBtnEnabled.

Hope that helps or at least can create a good starting point for solving your issue.

0
Tenfour04 On

I couldn't spot the error, but want to suggest using the flow combine operator instead of MediatorLiveData to hopefully simplify this enough to eliminate whatever the bug is:

@HiltViewModel
class CreateAccountEnterEmailViewModel @Inject constructor(
    appData: AppData,
) : ViewModel() {
    private var email by mutableStateOf("")
    private var phoneNumber by mutableStateOf("")
    private var _isPhoneNumberFieldVisibleState: MutableStateFlow<Boolean> = MutableStateFlow(appData.isPhoneNumberRequiredForRegistrationParam.orDef())
    val isPhoneNumberFieldVisible: LiveData<Boolean> = _isPhoneNumberFieldVisibleState.asLiveData()

    private val isEmailValid: Flow<Boolean> =
        snapshotFlow { email }
        .map { validateEmail(email = it) }

    private val isPhoneValid: Flow<Boolean> =
        snapshotFlow { phoneNumber }
            .map { validatePhone(phone = it) }

    val isContinueBtnEnabled: LiveData<Boolean> = 
        isEmailValid.combine(isPhoneValid) { email, phone ->
            email && phone
        }.asLiveData()

    fun updateEmail(email: String) {
        this.email = email
    }

    fun updatePhoneNumber(phoneNumber: String) {
        this.phoneNumber = phoneNumber
    }