Search code examples
androidkotlinmvvmviewmodelandroid-livedata

Progress in SeekBar and EditText with LiveData


I have TextInput, and SeekBar.

I Need to change seeker's value by text input value (numeric), and change the text value by seeker changed value. using View-Model and Live data binding

So, I have two mediator live datas in viewmodel class

ViewModel:

val progress: MediatorLiveData<Int> by lazy { MediatorLiveData<Int>() }
val progressText: MediatorLiveData<String> by lazy { MediatorLiveData<String>() }

which are observed by each other in init scope.

init {
  progress.apply { addSource(progressText) { postValue(it.toInt()) }}
  progressText.apply { addSource(progress) { postValue(it.toString()) }}
}

And the xml and data-binding are like:

<AppCompatSeekBar android:id="@+id/seekbar"
            style="@style/SeekerItemStyle"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:progress="@={viewModel.progress}" />

 <EditText android:id="@+id/input_progress"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@={viewModel.progressText}" />

enter image description here (consider the text is input)

But, here i got infinite loop, because one mediator's changes affect to other's and the other's change comes back to one.

Are here best practice and more clever solutions to solve this logic? Thanks.


Solution

    1. You don't need a MediatorLiveData here - MutableLiveData is enough because you just need to be able to change values.
    2. It's enough to have only one LiveData for progress and it should hold an Int value inside.

    Now the code:

    import androidx.lifecycle.MutableLiveData
    import androidx.lifecycle.ViewModel
    
    open class BasicViewModel: ViewModel() {
        val progress: MutableLiveData<Int> by lazy { MutableLiveData<Int>() }
    }
    

    And your layout could look like below (please note how viewModel.progress is converted to String for EditText)

    <layout xmlns:android="http://schemas.android.com/apk/res/android">
        <data>
            <variable
                name="viewModel"
                type="BasicViewModel"/>
        </data>
    
        <LinearLayout
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
    
            <EditText android:id="@+id/input_progress"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:inputType="numberDecimal"
                android:text="@={`` + viewModel.progress}" />
    
            <SeekBar
                android:id="@+id/seekbar"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:progress="@={viewModel.progress}" />
    
        </LinearLayout>
    </layout>
    

    Finally, your Activity could be as follows:

    class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
    
            val binding: ActivityMainBinding = DataBindingUtil.setContentView(
                    this, R.layout.activity_main)
            binding.setLifecycleOwner(this)
    
            val viewModel = ViewModelProviders.of(this).get(BasicViewModel::class.java)
            binding.viewModel = viewModel
        }
    }
    

    EDIT:

    If the text in the EditText is not identical to values in SeekBar then the approach will be different. However, it's still enough to have one LiveData<charSequence> object to implement such scenario. So, let's imagine that EditText should show a float value like progress/100. Then, it could look like as below:

    BasicViewModel.kt

    package com.example.android.databinding.basicsample.data
    
    import androidx.lifecycle.LiveData
    import androidx.lifecycle.MutableLiveData
    import androidx.lifecycle.Transformations
    import androidx.lifecycle.ViewModel
    
    open class BasicViewModel: ViewModel() {
        val progressLiveData = MutableLiveData("0.0")
    
        fun updateEditText(percentage: Int) {
            progressLiveData.value = percentage.toFloat().div(100).toString()
        }
    
        fun onEditTextTyped(): LiveData<Int> {
            return Transformations.switchMap(progressLiveData, {
                val liveData = MutableLiveData<Int>()
                try {
                    liveData.value = it.toString().toFloat().times(100f).toInt()
                } catch (e: NumberFormatException) {
                    // reset the progress bar if the progress text is invalid
                    liveData.value = 0
                }
                liveData
            })
        }
    }
    

    activity_main.xml

    <layout xmlns:android="http://schemas.android.com/apk/res/android">
        <data>
            <variable
                name="viewModel"
                type="com.example.android.databinding.basicsample.data.BasicViewModel"/>
        </data>
    
        <LinearLayout
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
    
            <androidx.appcompat.widget.AppCompatEditText
                android:id="@+id/input_progress"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:text="@={viewModel.progressLiveData}" />
    
            <SeekBar
                android:id="@+id/seekbar"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:progress="@{viewModel.onEditTextTyped}"
                android:onProgressChanged="@{(seekBar, progress, fromUser) -> viewModel.updateEditText(progress)}" />
    
        </LinearLayout>
    </layout>