0

I am having trouble understanding how the Fragment + ViewModel paradigm works with a View like an EditText.

It being an EditText, it's gonna obviously be modified within the View (Fragment). But I also want to be able to modify it within the ViewModel: e.g. to erase its text.

Here's the code in the Fragment class:

public void onActivityCreated(@Nullable Bundle savedInstanceState) {
...
        comment = mViewModel.getComment();
        comment.observe(getViewLifecycleOwner(), new Observer<String>() {
            @Override
            public void onChanged(String s) {
                commentView.setText(s);
            }
        });
...
        commentView.addTextChangedListener(new TextWatcher() {
            @Override
            public void afterTextChanged(Editable s) {
                mViewModel.setComment(String.valueOf(s));
            }
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) { }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) { }
        });

As you can see, I set an observer so when I change the value of the MutableLiveData, the View changes. And I set a watcher so when I (when using the App) change the value of the View, the MutableLiveData changes.

Here's the code of the ModelView class:

public void addRegister() {
...
String comment = this.comment.getValue();
...
this.comment.setValue("");

When I run the App no error pops up, but it hangs. I guess because of an infinite loop. How should I approach EditTexts with this View + ViewModel paradigm? What am I not understanding?

Thanks a lot in advance!

DidacC
  • 93
  • 2
  • 13

3 Answers3

2

As the accepted answer did not work for me in all cases (when the text was changed in the ViewModel by other means than by the EditText itself), and I also did not want to go with databinding, I came up with the following solution, where a flag keeps track of updates, which where initiated by the TextWatcher, and breaks the loop when the observer is called:

Here is my code in Kotlin. For the activity:

class SecondActivity : AppCompatActivity() {

    /** Flag avoids endless loops from TextWatcher and observer */
    private var textChangedByListener = true
    private val viewModel by viewModels<SecondViewModel>()
    private lateinit var binding:SecondActivityBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = SecondActivityBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.editText.addTextChangedListener(object: TextWatcher {
            override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3:     Int) {            }
            override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {                }

            override fun afterTextChanged(editable: Editable?) {
                textChangedByListener = true
                viewModel.editText = editable.toString()
            }

        })
        viewModel.editTextLiveData.observe(this) { text -> setEditTextFromViewModel(text) }
    }

    private fun setEditTextFromViewModel(text: String?) {
        if (!textChangedByListener) {
            //text change was not initiated by the EditText itself, and
            //therefore EditText does not yet contain the new text.
            binding.editText.setText(text)
        } else {
            //Don't move that outside of else, because it would then
            //immediately overwrite the value set by TextWatcher
            //which is triggered by the above setText() call.
            textChangedByListener = false
        }
    }

}

And for completeness also the ViewModel:

class SecondViewModel() : ViewModel()
{
    var editText: String
        get() {
            return editTextLiveData.value ?: "InitialLiveData"
        }
        set(value) {
            editTextLiveData.value = value
        }

    var editTextLiveData = MutableLiveData<String>()
}

Just in case, you are not familiar with view binding: You can replace

binding.editText

with

findViewById(R.id.editTextId) as EditText.

user2808624
  • 2,502
  • 14
  • 28
0

You can use two-way databinding for this:

  • When the user enters text: the live data will be updated
  • If you set the live data value programmatically, the EditText content will be updated

You should be able to remove both listeners in your activity, as data binding does that for you.

build.gradle:

android {
    dataBinding {
        enabled = true
    }
}

layout:

  • add a <layout> element at the top level
  • define a variable for your viewmodel
  • connect your EditText to the view model
<layout>
    <data>
        <variable
            name="viewModel"
            type="com.mycompany.AddRegisterViewModel" />
    </data>
    <EditText
                android:id="..."
                android:layout_width="..."
                android:layout_height="..."
                android:text="@={viewModel.getComment()}" />
</layout>

Fragment (sorry, kotlin example):

  • Hook the viewModel field in the xml with your viewmodel object:
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        val binding: MyFragmentBinding = DataBindingUtil.inflate(inflater, R.layout.my_fragment, container, false)
        binding.setViewModel(myViewModel)

Note that you need the equals sign, @= to have two-way databinding. If you just use @{viewModel.getComment()}, then the edit text will be updated if you programmatically set the live data value, but the other way won't work.

Notes:

  • You can use ObservableField instead of MutableLiveData for data binding, if you prefer
  • Maybe you can reference the live data in the xml with a field reference instead of method reference like @={viewModel.comment}

Reference: Android documentation for two-way databinding: https://developer.android.com/topic/libraries/data-binding/two-way

Carmen
  • 1,574
  • 17
  • 18
  • Thanks a lot for the response, I'll definitely look into this. Although I've read some negative comments about the Databinding library, saying that putting logic or functions in the static xml files is bad practice. What do you think? Is Android's Databinding a standard nowadays? – DidacC Jan 23 '20 at 11:38
  • You should avoid putting any significant logic in the xml files for sure. But here, as a typical basic databinding example, there's really no logic: it's just mapping a view to a viewmodel field (Usually I expose the `MutableLiveData`s as fields (kotlin `val`s) so I would normally use `android:text="@={viewModel.comment}"` instead of a function `getComment()`) – Carmen Jan 23 '20 at 13:13
0

in your observer for comment liveData, just unregister the TextWatcher first, then after setText from comment liveData, re-register the TextWatcher, it should be fine :)

Devara
  • 75
  • 2
  • What do you mean unregister the TextWatcher? – DidacC Jan 31 '20 at 10:16
  • currently your code is `commentView.addTextChangedListener(anonymouseTextWatcherClass)` . You can do this, first initialise textWatcher object like `textWatcher = TextWatcher()` and save it as field variable. Then when you about to set value to your EditText, unregister the textWatcher first by call this function `commentView.removeTextChangedListener(textWatcher);`, then you can safely set value without trigger the textWatcher. After you done set value, register your textWatcher again – Devara Feb 03 '20 at 04:11
  • Good to hear :) maybe u can set it as accepted answer – Devara Feb 06 '20 at 10:16