MutableLiveData not updating in UI

DevinM picture DevinM · Dec 31, 2019 · Viewed 8.7k times · Source

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.

Answer

Anatolii picture Anatolii · Jan 3, 2020

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.