Search code examples
androidkotlinandroid-livedataandroid-viewmodelmediatorlivedata

Add multiple source to MediatorLiveData and change its value


Basically I have a screen, and there are a few EditTexts and a Button. Users have to fill in all fields otherwise the Button is disabled. I am using DataBinding to achieve this. Below is my code in the viewmodel.

val isNextEnabled = MediatorLiveData<Boolean>()
isNextEnabled.apply {
            addSource(field1LiveData) {
                isNextEnabled.value =
                    it != null
                            && field2LiveData.value != null
                            && field3LiveData.value != null
            }
            addSource(field2LiveData) {
                isNextEnabled.value =
                    it != null
                            && field1LiveData.value != null
                            && field3LiveData.value != null
            }
            addSource(field3LiveData) {
                isNextEnabled.value =
                    it != null
                            && field2LiveData.value != null
                            && field1LiveData.value != null
            }
        }

In the xml

<Button
    android:enabled="@{viewmodel.isNextEnabled}"
    .
    .
    .
</Button>

Everything works fine as expected. But the logic above looks cumbersome. What if I have more EditText ? The code would be painful to write/maintain.

Is there any way I can simplify it?


Solution

  • Ultimately you have a UseCase/Logic where you decide when the next button is enabled.

    I think you should separate the logic into useCases where it makes sense.

    E.g.

    // update these when they change in the UI for e.g.
        val field1Flow: Flow<Boolean> = flow { ... }
        val field2Flow: Flow<Boolean> = flow { ... }
        
        
        val nextButtonState = combine(field1Flow, field2Flow) { f1, f2 -> 
            f1 && f2
        }.collect { state -> 
            // use your state.
     }
    

    Now... if you need special logic and not just two-boolean algebra here, you can always extract it into use-cases that return more flows.

    Or map it or various operations you could do:

    E.g.

    class YourUseCase() {
    
       operator fun invoke(field1: Boolean, field2: Boolean) {
          // Your Logic
          return field1 && field2 
       } 
    }
    
    // And now...
    val _nextButtonState = combine(field1Flow, field2Flow) { f1, f2 -> 
        YourUseCase(f1, f2)
    }
    
    val _uiState = _nextButtonState.transformLatest { 
       emit(it) // you could add a when(it) { } and do more stuff here
    }
    
    // And if you don't want to change your UI to use flows, you can expose this as live data
     val uiState = _uiState.asLiveData()
    

    Keep in mind this is Pseudo-code written on SO.. not even Notepad ;)

    I hope that makes a bit of sense. The idea is to separate the bits into use-cases (that you can ultimately test in isolation) and to have a flow of data. When buttons change state, the fieldNFlow emits the values and this triggers the whole chain for you.

    If you have the latest Coroutines (2.4.0+) you can use the new operators to avoid using LiveData, but overall, I'd try to think in that direction.

    Lastly, your liveData code with a mediator is not bad, I'd at the very least, extract the "logic" into 3 different useCases so it's not all together in a series of if/else statements.

    A word of caution: I haven't used Databinding in over 3(?) years, I'm personally not a fan of it so I cannot tell you if it would cause a problem with this approach.