0

I am using Android LiveData in 3 different EditText. I have to show the result of multiplying the values of the first two EditText into the third EditText. I took advantage of an advice given to me on this site, and actually the third value is updated with the result of the multiplication of the first two. The problem is that the update does not happen live, but only happens when I leave and re-enter the activity. I am attaching the XML file, the activity, and the viewmodel.

XML:

<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">


<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintHorizontal_bias="0.5"
    app:layout_constraintStart_toStartOf="parent"
    tools:context=".MainActivity">
         
            <EditText
                android:id="@+id/num1"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_centerHorizontal="true"
                android:layout_centerVertical="true"
                android:backgroundTint="@color/white"
                android:inputType="number" />

            <EditText
                android:id="@+id/num2"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_centerHorizontal="true"
                android:layout_centerVertical="true"
                android:backgroundTint="@color/white"
                android:inputType="numberDecimal" />

            <EditText
                android:id="@+id/num3"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_centerHorizontal="true"
                android:layout_centerVertical="true"
                android:backgroundTint="@color/white"
                android:inputType="numberDecimal" />

    </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

Activity

class MainActivity: AppCompatActivity() {
private lateinit var binding: MainActivityBinding

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.main_activity)
    binding = DataBindingUtil.setContentView(
        this,
        R.layout.main_activity
    )

    viewModel=
        ViewModelProvider(this, factory)[MainActivityViewModel::class.java]

    initView(binding)
}

private fun initView(
    binding:
    MainActivityBinding
) {
    
        viewModel.num1.value = root?.num1?: 0
        viewModel.num2.value = root?.num2?: 0.0

       viewModel.num1.observe(lifecycleOwner, Observer { newNum1->
            binding.num1.setText(
                newNum1.toString()
            )
        })

        viewModel.num2.observe(lifecycleOwner, Observer { newNum2->
            binding.num2.setText(
                newNum2.toString()
            )
        })

binding.num1.addTextChangedListener(object : TextWatcher {
            override fun afterTextChanged(s: Editable?) {

                viewModel.num1.value =
                    binding.num1.text?.toString()?.toInt()
                        ?: 0

            }

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

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

        binding.num2.addTextChangedListener(object : TextWatcher {
            override fun afterTextChanged(s: Editable?) {

                viewModel.num2.value =
                    binding.num2.text?.toString()?.toDouble()
                        ?: 0.0

            }

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

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

            }
        })

        fun <A, B> LiveData<A>.combineWith(b: LiveData<B>): LiveData<Pair<A?, B?>> =
            MediatorLiveData<Pair<A?, B?>>().apply {
                var lastA: A? = this@combineWith.value
                var lastB: B? = b.value

                addSource(this@combineWith) {
                    lastA = it
                    value = Pair(lastA, lastB)
                }

                addSource(b) {
                    lastB = it
                    value = Pair(lastA, lastB)
                }
            }

        viewModel.num1.combineWith(viewModel.num2)
            .observe(
                this,
                Observer { (first, second) ->
                    if (first != null && second != null) {
                        binding.num3.setText((first * second).toString())
                    }
                }
            )
    }

    binding.num1.isFocusableInTouchMode = true
    binding.num2.isFocusableInTouchMode = true
    binding.num3.isFocusableInTouchMode = true

}

}

ViewModel

class RapportiAltriCostiViewModel(private val repositoryDB: DbRepository) : ViewModel() {


var num1= MutableLiveData<Int>()
var num2= MutableLiveData<Double>()
}

Would anyone know how to solve?

Thank you for your patience and help!

UPDATE

I tried with TextWatcher but it goes in loop:

binding.num1.addTextChangedListener(object : TextWatcher {
        override fun afterTextChanged(s: Editable?) {

            viewModel.num1.value =
                binding.num1.text?.toString()?.toInt()
                    ?: 0

        }

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

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

        }
    })

    binding.num2.addTextChangedListener(object : TextWatcher {
        override fun afterTextChanged(s: Editable?) {

            viewModel.num2.value =
                binding.num2.text?.toString()?.toDouble()
                    ?: 0.0

        }

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

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

        }
    })

And I can't remove the TextWatcher after assigning the value, as I read on another question on the site, because I need them to always listen.

Thanks for the patience once again!

qwerty123
  • 43
  • 4
  • In your situation, You need `TextWatcher`. If you want to use `LiveData`, `setValue()` in `override afterTextChanged()`,But It does not need `LiveData` in this case I think. – liveAnyway May 18 '21 at 13:12
  • I updated the question by adding the attempt I made with the TextWatcher, but that didn't work either because it went into a loop. – qwerty123 May 18 '21 at 13:20
  • Are `binding.num1` ,`bining.num2` have `TextWatcher`? If Is `binding.num3` just for output `View` of multiply, `binding.num3` don't need `TextWatcher` – liveAnyway May 18 '21 at 13:25
  • Yes, sorry, i copied the wrong code, now it is right. – qwerty123 May 18 '21 at 13:43

1 Answers1

0

Something similar to this. To avoid cyclic updates you may just compare new value inside onFirstChanged/onSecondChanged with value in your liveData and skip liveData.value = newValue in that way.

class MainActivity : AppCompatActivity() {
    private lateinit var binding: MainActivityBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.main_activity)
        binding = DataBindingUtil.setContentView(
            this,
            R.layout.main_activity
        )

        viewModel =
            ViewModelProvider(this, factory)[MainActivityViewModel::class.java]

        initView(binding)
    }

    private fun initView(
        binding:
        MainActivityBinding
    ) {
        binding.num1.listenChanges { viewModel.onFirstChanged(it) }
        binding.num2.listenChanges { viewModel.onSecondChanged(it) }

        viewModel.num1
            .observe(
                lifecycleOwner,
                Observer { num1Value ->
                    binding.num1.setText(num1Value.toString())
                }
            )
        viewModel.num2
            .observe(
                lifecycleOwner,
                Observer { num2Value ->
                    binding.num2.setText(num2Value.toString())
                }
            )
        viewModel.num3
            .observe(
                lifecycleOwner,
                Observer { result ->
                    binding.num3.setText(result.toString())
                }
            )
    }

    binding.num1.isFocusableInTouchMode = true
    binding.num2.isFocusableInTouchMode = true
    binding.num3.isFocusableInTouchMode = true

}

private fun EditText.listenChanges(textChanged: (String) -> Unit) {
    addTextChangedListener(object : TextWatcher {
        override fun afterTextChanged(s: Editable?) {
        }

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

        override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
            textChanged(s.toString())
        }
    })
}

class RapportiAltriCostiViewModel(private val repositoryDB: DbRepository) : ViewModel() {

    val num1 = MutableLiveData<Int>(0)
    val num2 = MutableLiveData<Double>(0.0)
    val num3: LiveData<Double>
        get() = num1.combineWith(num2) { first, second ->
            (first ?: 0) * (second ?: 0.0)
        }

    fun onFirstChanged(newValue: Int) {
        if (num1.value != newValue) {
            num1.value = newValue
        }
    }

    fun onSecondChanged(newValue: Double) {
        if (num2.value != newValue) {
            num2.value = newValue
        }
    }

    private companion object {
        private fun <A, B, R> LiveData<A>.combineWith(b: LiveData<B>, combine: (A?, B?) -> R?): LiveData<R> =
            MediatorLiveData<R>().apply {
                var lastA: A? = this@combineWith.value
                var lastB: B? = b.value

                addSource(this@combineWith) {
                    lastA = it
                    value = combine.invoke(lastA, lastB)
                }

                addSource(b) {
                    lastB = it
                    value = combine.invoke(lastA, lastB)
                }
            }
    }
}
Romadro
  • 626
  • 4
  • 10
  • Sorry for the delay, I have another problem: in this way everything works well, but the cursor does not go in the right position. Would you know how to solve? For information you could look at my question: https://stackoverflow.com/questions/67719529/cursor-position-in-edittext-with-textwatcher-and-livedata/67720346#67720346 – qwerty123 May 27 '21 at 10:49