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}" />
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.
MediatorLiveData
here - MutableLiveData
is enough because you just need to be able to change values.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>