6

I have simple view with edit text and 2 buttons and I want to give user ability to increse/decrese amount by 1 with buttons or he can manually write amount in edit text widget, but this value can't be less than 0 and greater than 10. I'm using MVVM architecture with LiveData and databinding. Currently in ViewModel I've two variables _amount: Int and amount:MutableLiveData. I'm incrmenting/decrementing _amount and then assign new value to MutableLiveData. It's working but I'm wondering if it is possible to achive same result using only one variable to store amount i.e amount:MutableLiveData

ViewModel
class TestActivityVM : ViewModel() {

    val amount = MutableLiveData<String>()
    private var _amount = 0

    init {
        amount.value = _amount.toString()
    }

    fun increment() {
        Log.d("TestActivityVM", "The amount is being increment, current value = ${amount.value}")

        //increment amount value by 1 if amount is less than 10
        if(_amount < 10) {
            amount.value = (++_amount).toString()
        }
    }

    fun decrement() {
        Log.d("TestActivityVM", "The amount is being decrement, current value = ${amount.value}")

        //decrement amount value by 1 if amount is greater than 0
        if(_amount > 0) {
            amount.value = (--_amount).toString()
        }
    }

    val amountValidator: TextWatcher = object : TextWatcher {
        override fun afterTextChanged(s: Editable?) {
            _amount = if(s.toString().isEmpty()) 0 else s.toString().toInt()

            if(_amount > 10) {
                _amount = 10
                amount.value = _amount.toString()
            }
        }

        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
        }

        override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
        }

    }
}
Activity
class TestActivity : AppCompatActivity() {

    private lateinit var testBinding: ActivityTestBinding
    private lateinit var testVM: TestActivityVM

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        testVM = ViewModelProviders.of(this).get(TestActivityVM::class.java)

        testBinding = DataBindingUtil.setContentView<ActivityTestBinding>(this, R.layout.activity_test).also {
            it.lifecycleOwner = this
            it.testVM = testVM
        }
    }
}
Layout
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="testVM"
            type=".TestActivityVM" />
    </data>


    <android.support.constraint.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".TestActivity">

        <Button
            android:id="@+id/minusBtn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginTop="16dp"
            android:text="-"
            android:onClick="@{() -> testVM.decrement()}"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/amountET" />

        <Button
            android:id="@+id/plusBtn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:layout_marginEnd="8dp"
            android:text="+"
            android:onClick="@{() -> testVM.increment()}"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/amountET" />

        <EditText
            android:id="@+id/amountET"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            android:layout_marginEnd="8dp"
            android:ems="10"
            android:inputType="numberSigned"
            android:text="@={testVM.amount}"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:addTextChangedListener="@{testVM.amountValidator}"/>

        <TextView
            android:id="@+id/textView4"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="16dp"
            android:layout_marginTop="84dp"
            android:text="Value of amount Live data:"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/minusBtn" />

        <TextView
            android:id="@+id/textView5"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="32dp"
            android:layout_marginTop="8dp"
            android:text="@{testVM.amount}"
            android:textSize="24sp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textView4" />

    </android.support.constraint.ConstraintLayout>
</layout>

Remzo
  • 149
  • 3
  • 12

3 Answers3

11

You can use amount.value to get the current amount, instead of using another variable. The LiveData should be of Int, not String, as that's the data type you're dealing with. Here's how your code could look like:

class TestActivityVM : ViewModel() {

    val amount = MutableLiveData<Int>().apply {
        value = 0
    }

    fun increment() {
        Log.d("TestActivityVM", "The amount is being increment, current value = ${amount.value}")

        //increment amount value by 1 if amount is less than 10
        amount.value?.let { a ->
            if (a < 10) {
                amount.value = a + 1
            }
        }
    }
cd1
  • 15,908
  • 12
  • 46
  • 47
  • Yes, I agree. Generally it should be Int type, but I am using two-way databinding and there is no autoconversion between received String to Int. How to solve this problem? – Remzo Jul 26 '19 at 13:29
  • @Remzo, use BindingAdapters and InverseBindingAdapters. You can remove your textwatcher logic. It looks like validation in textwatcher can be done just with the BinidngAdapters. – Sanlok Lee Jul 26 '19 at 23:22
0

change amount to type MutableLiveData

fun increment() {
    Log.d("TestActivityVM", "The amount is being increment, current value = ${amount.value}")

    //increment amount value by 1 if amount is less than 10
    if(amount.value!! < 10) amount.value = amount.value!! +1
}

fun decrement() {
    Log.d("TestActivityVM", "The amount is being decrement, current value = ${amount.value}")

    //decrement amount value by 1 if amount is greater than 0
    if(amount.value!! > 0) {
        amount.value = amount.value!! -1
    }
}
Horrorgoogle
  • 7,858
  • 11
  • 48
  • 81
Neha Rathore
  • 429
  • 2
  • 9
0

Ideally recommend approach is to observe immutable livedata object in activity . Instead of observing mutable variable from VM , create some method returning liveddata , so that activity should not modify the mutable object.

Also avoid using Android components/ APIs in VM to make VM testable.

Ashok Kumar
  • 1,226
  • 1
  • 10
  • 14