Add multiple source to MediatorLiveData and change its value

1.4k Views Asked by At

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?

1

There are 1 best solutions below

2
On BEST ANSWER

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.