42

UPDATE: If i move to another fragment and return to this one the TextView gets updated...

I am unable to get the MutableLiveData in the UI to update to a new value either by using setValue() or postValue(). I can get this to work by changing the MutableLiveData to ObservableData but the point is to use LiveData not ObservableData.

I have similar things working in other views without an issue. Unsure whats happening here... The only thing I can think of is that I am jumping back and forth between a camera activity (via intent) and this fragment. On the activity result I am setting data from the fragment to the ViewModel.

I dont believe that I need to set any observer for the value in the fragment since i have 2 way databinding between the XML and ViewModel.

my_fragment.XML

<?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="myViewModel"
            type="com.example.myapplication.MyViewModel" />
    </data>

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

            <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@={myViewModel.photosCount}"
            tools:text="1" />

           <Button
               android:id="@+id/btnTakePhoto"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:text="Take Picture"
               android:onClick="@{myViewModel::navClicked}"/>

    </LinearLayout>

</layout>

MyViewModel.java

private MutableLiveData<String> photosCount = new MutableLiveData<>();
private MutableLiveData<Boolean> takePhoto = new MutableLiveData<>();

public MyViewModel() {
    photosCount.setValue("0"); // This correctly sets the value to "0" in UI
    takePhoto.setValue(true);
}

public MutableLiveData<String> getPhotosCount() {
    return photosCount;
}

public MutableLiveData<Boolean> getTakePhoto() {
    return takePhoto;
}

public void storeImage() {
    ....
    Log.d(TAG, "Updating UI count to: " + String.valueOf(count)); // this shows the correct value that should be updated in the UI
    photosCount.setValue(String.valueOf(count));
    Log.d(TAG, "Updated value: " + photosCount.getValue()); // this shows the correct updated value
    ...
}

public void navClicked(@NonNull View view) {
    if (view.getId() == R.id.btnTakePhoto) {
        takePhoto.setValue(true);
    }
}

Now, since LiveData does not guarantee updating the UI at the time of changing the value I thought this might be a binding dispatch issue. That hasent resolved the issue either...

MyFragment.java

private MyViewModel myViewModel;
MyFragmentBinding binding;

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    super.onCreateView(inflater, container, savedInstanceState);

    myViewModel = new ViewModelProvider(this).get(MyViewModel.class);

    binding = DataBindingUtil.inflate(inflater, R.layout.my_fragment, container, false);
    binding.setMyViewModel(myViewModel);

    return binding.getRoot();
}

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    viewModel.getTakePhoto().observe(getViewLifecycleOwner(), takePhoto -> {
        if (takePhoto) {
            Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);

            if (intent.resolveActivity(getActivity().getPackageManager()) != null) {
                File photo = viewModel.createImageFile();
                if (photo != null) {
                    Uri photoURI = FileProvider.getUriForFile(getActivity(),"com.example.fileprovider", photo);
                    intent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
                    this.startActivityForResult(intent, Constants.RequestCodes.MY_REQUEST_IMAGE_CAPTURE);
                }
            }
        }
    });
}

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (resultCode == Activity.RESULT_OK) {
        if (requestCode == Constants.RequestCodes.MY_REQUEST_IMAGE_CAPTURE) {
            viewModel.storeImage();
        }
    }
}

Again, If you switch photosCount from MutableLiveData to ObservableData the problem is fixed however this is not LiveData.

DevinM
  • 1,112
  • 1
  • 12
  • 29

8 Answers8

89

I am unable to get the MutableLiveData in the UI to update to a new value either by using setValue() or postValue(). I can get this to work by changing the MutableLiveData to ObservableData but the point is to use LiveData not ObservableData.

You probably don't set a lifecycle owner for your binding object. From the JAVADOC for setLifecycleOwner() this assumption sounds even more convincing (please see it here):

If a LiveData is in one of the binding expressions and no LifecycleOwner is set, the LiveData will not be observed and updates to it will not be propagated to the UI.

So, I took a closer look at your code.

Observation

I noticed that you don't show how your ViewModel and binding are created. So, I have created a simple project meeting your requirements and updating the UI properly - setValue or postValue of LiveData were changing the corresponding View values seamlessly. Then, I filled in the missing pieces in your code based on my working example. Please see the updated code below.

Relevant Code

MainViewModel

public class MainViewModel extends ViewModel {
    private MutableLiveData<String> photosCount = new MutableLiveData<>();
    private MutableLiveData<Boolean> takePhoto = new MutableLiveData<>();

    public MainViewModel() {
        photosCount.setValue("0");
        takePhoto.setValue(true);
    }

    public MutableLiveData<String> getPhotosCount() {
        return photosCount;
    }

    public MutableLiveData<Boolean> getTakePhoto() {
        return takePhoto;
    }

    public void createImageFile() {
        ...
        photosCount.setValue(String.valueOf(count));
        ...
    }
    ...
}

fragment_main.xml

<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="viewModel"
            type="MainViewModel"/>
    </data>

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@={viewModel.photosCount}" />
    </LinearLayout>
</layout>

MainFragment

public class MainFragment extends Fragment {
    private MainViewModel mainViewModel;
    private FragmentMainBinding binding;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        binding = DataBindingUtil.inflate(inflater, R.layout.fragment_main, container,
                false);
        binding.setLifecycleOwner(this);

        mainViewModel = ViewModelProviders.of(this).get(MainViewModel.class);
        binding.setViewModel(mainViewModel);

        return binding.getRoot();
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        mainViewModel.getTakePhoto().observe(getViewLifecycleOwner(), takePhoto -> {
            if (takePhoto) {
                Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);

                if (intent.resolveActivity(getActivity().getPackageManager()) != null) {
                    File photo = mainViewModel.storeImage();
                    if (photo != null) {
                        Uri photoURI = FileProvider.getUriForFile(getActivity(),"com.example.fileprovider", photo);
                        intent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
                        this.startActivityForResult(intent, 0);
                    }
                }
            }
        });
        binding.executePendingBindings();
    }
}

Conclusion

Please note that a lifecycle owner must be set for binding as binding.setLifecycleOwner(this). MainViewModel must be set for binding too. Otherwise, it won't work properly. When in doubt, always check the official documentation here.

Anatolii
  • 14,139
  • 4
  • 35
  • 65
  • 1
    fixed my issue! With kotlin + ktx in the fragment one can do: `binding = FragmentBinding.inflate(inflater)` followed by `binding.lifecycleOwner = viewLifecycleOwner` – hba Dec 10 '21 at 23:12
10

I'm not sure if this is the answer to your question, but changing

myViewModel = new ViewModelProvider(this).get(MyViewModel.class); to

myViewModel = new ViewModelProvider(requireActivity()).get(MyViewModel.class);

did the trick for me. According to the official docs, when initializing a ViewModel in a fragment, you need to use requireActivity()

Ian
  • 324
  • 5
  • 12
  • 1
    This helped me. Thank you. In fragments, requireActivity() in stead of 'this' is needed for ViewModelProvider. – LXJ Nov 25 '21 at 10:14
6

in fragment i just set binding lifecycleowner to this fragment's lifecycleowner like:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    binding.viewmodel = viewModel
    binding.lifecycleOwner = this    // add this line
    init()
}
1

From the official documents:

/**
 * Sets the {@link LifecycleOwner} that should be used for observing changes of
 * LiveData in this binding. If a {@link LiveData} is in one of the binding expressions
 * and no LifecycleOwner is set, the LiveData will not be observed and updates to it
 * will not be propagated to the UI.
 *
 * @param lifecycleOwner The LifecycleOwner that should be used for observing changes of
 *                       LiveData in this binding.
 */z

Required Steps:

  1. Make sure to set activityOrFragmentBinding.lifecycleOwner = this after intilizing the view binding. activityOrFragmentBinding replace it with your binding variable name.
  2. Always set you ViewModel to data binding.
Muhammad Maqsood
  • 1,622
  • 19
  • 25
0

i think there is a note may help.

according to documentation :

In most cases, an app component’s onCreate() method is the right place to begin observing a LiveData object

and for fragments at end of this article

Edit (14 may 2018): Google decided to implement solution 4 directly in support library 28.0.0 and AndroidX 1.0.0. I now recommend that you register your observers using the special lifecycle returned by getViewLifecycleOwner() in onCreateView(). I like to think that this article played a part in Google fixing this issue and picking up the proposed solution.

onCreateView() is the right place to begin observing live data.

therefore there is no need to observe live data each time in onResume of fragment.

Mojtaba Haddadi
  • 1,346
  • 16
  • 26
0

I dont like this answer but its the only one I have at this time. I will still accept the correct answer using 2 way data binding if/when it is presented.

Until then, if you have a situation where 2 way data binding is NOT updating your UI then remove the 2 way data binding. Let your fragment observe the LiveData and update the element in the UI.

XML - remove the 2 way data binding @={viewModel.photosCount}

<TextView
   android:id="@+id/textView"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:text="0"/>

ViewModel - photosCount.setValue("0") in constructor can be removed OR your XML can not set the text if you like. Either is acceptable.

public ViewModel() {
    photosCount.setValue("0"); // this isnt necessary anymore depending on how you like to handle it.
    ....
}

Fragment - Move the observers back to onViewCreated(). This is where I prefer to place them as it keeps my on onCreateView() clean and dedicated to inflating the view, creating the binding, updating the action bar and returning the view. Add the LiveData observer to update the UI.

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);

    viewModel.getPhotosTaken().observe(getViewLifecycleOwner(), photosTaken -> {
        binding.textView.setText(photosTaken);
    });

    ....
}
DevinM
  • 1,112
  • 1
  • 12
  • 29
0

This may help someone....

I was looking to set A Textviews text from a MediatorLiveData() after it was observed in A model view, It turns out that ObservableField() was perfect it works like so:-

        var hasitbeenhacked = ObservableField<String>()
    
        val responsebooolean: LiveData<String>
            get() = CheckPasswordRepository().passwordResult(password!!)
    
    /// Called via Databinding
    
        fun onSubmitClicked() {
            if(password.isNullOrEmpty()){
          
                onRequest?.onStarted()
                onRequest?.onFailure("A Field Is Empty")
    
                return
    
            }
    
            onRequest?.onStarted()
            var apiResponse = CheckPasswordRepository().passwordResult(password!!)
    
    
            val result: MediatorLiveData<String> = MediatorLiveData<String>()
    
            result.addSource(responsebooolean, Observer<String?> {
    
                if (it?.contains("true")!!) {
    
                    hasitbeenhacked.set( "Unfortunately Your Password Has Been Compromised")
    
    
                } else {
                    hasitbeenhacked.set("Your Password Is Fine")
    
                }
            
            })
    
            onRequest?.onSuccess(result)

and the XML file looks like

<?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"
    tools:context=".views.CheckPassword">

    <data>

        <variable
            name="viewmodelvar"
            type="com.example.haveibeenh4cked.viewmodels.checkpassword.CheckPasswordViewModel" />

        <variable
            name="fragment"
            type="com.example.haveibeenh4cked.views.CheckPassword" />
    </data>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <com.google.android.material.textfield.TextInputLayout
                android:id="@+id/textInputLayout"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="  Enter Password"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintVertical_bias="0.361">

                <com.google.android.material.textfield.TextInputEditText
                    android:id="@+id/password_test"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:imeOptions="actionDone"
                    android:singleLine="true"
                    android:text="@={viewmodelvar.password}"
                    tools:layout_editor_absoluteX="4dp"
                    tools:layout_editor_absoluteY="83dp" />

            </com.google.android.material.textfield.TextInputLayout>

            <TextView

                android:id="@+id/checkAPI"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="416dp"
                android:text="@={viewmodelvar.hasitbeenhacked}"
                app:layout_constraintTop_toTopOf="parent"
                tools:layout_editor_absoluteX="-16dp" />

            <Button
                android:id="@+id/submit_password_test"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginTop="340dp"
                android:background="@color/colorPrimary"
                android:onClick="@{() -> viewmodelvar.onSubmitClicked()}"
                android:text="Submit"
                android:textStyle="bold"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintHorizontal_bias="0.5"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent" />

        </androidx.constraintlayout.widget.ConstraintLayout>
    </FrameLayout>
</layout>
SCouto
  • 7,808
  • 5
  • 32
  • 49
josh marsden
  • 57
  • 1
  • 2
0

I also faced a similar issue not long ago. I had a 2 fragments in a navigation graph and the second fragment which happens to be the details of an item in the first fragment was not getting updated by the ViewModel class even though my debug logs show that the data is being set.

It turned out that a new ViewModel instance gets created every time I called ViewModelProviders.of(this).get(MainViewModel.class);.

The solution was to make the ViewModel a singleton although this may not be the most desirable solution.