MutableLiveData 未在 UI 内更新

MutableLiveData not updating in UI

更新: 如果我移动到另一个片段并 return 到这个片段,TextView 会更新...

我无法使用 setValue() 或 postValue() 使 UI 中的 MutableLiveData 更新为新值。我可以通过将 MutableLiveData 更改为 ObservableData 来使其工作,但重点是使用 LiveData 而不是 ObservableData。

我有类似的东西在其他视图中工作,没有问题。不确定这里发生了什么......我唯一能想到的是我在相机 activity(通过意图)和这个片段之间来回跳跃。在 activity 结果上,我将数据从片段设置到 ViewModel。

我认为我不需要为片段中的值设置任何观察器,因为我在 XML 和 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);
    }
}

现在,由于 LiveData 不保证在更改值时更新 UI,我认为这可能是一个绑定调度问题。那也没有解决问题...

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();
        }
    }
}

同样,如果将 photosCount 从 MutableLiveData 切换到 ObservableData,问题就解决了,但这不是 LiveData。

我认为有一个注释可能会有所帮助。

根据documentation

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

以及 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() 是开始观察实时数据的正确位置。

因此不需要每次都在onResume片段中观察实时数据。

我不喜欢这个答案,但这是我目前唯一的答案。我仍然会接受使用 2 种方式数据绑定的正确答案if/when。

到那时,如果您遇到 2 路数据绑定 NOT 更新您的 UI 的情况,请删除 2 路数据绑定。让您的片段观察 LiveData 并更新 UI.

中的元素

XML - 删除 2 路数据绑定 @={viewModel.photosCount}

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

ViewModel - 构造函数中的 photosCount.setValue("0") 可以删除,或者如果您愿意,您的 XML 无法设置文本。两者皆可。

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

Fragment - 将观察者移回 onViewCreated()。这是我更喜欢放置它们的地方,因为它使我的 onCreateView() 保持干净并专用于膨胀视图、创建绑定、更新操作栏和返回视图。添加 LiveData 观察器以更新 UI.

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

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

    ....
}

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.

您可能没有为您的 binding 对象设置生命周期所有者。从JAVADOC for setLifecycleOwner()这个假设听起来更有说服力(请看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.

所以,我仔细查看了您的代码。

观察

我注意到您没有说明 ViewModel 和绑定是如何创建的。因此,我创建了一个满足您的要求并正确更新 UI 的简单项目 - setValueLiveDatapostValue 正在无缝更改相应的 View 值。然后,我根据我的工作示例填写了您的代码中缺失的部分。请参阅下面的更新代码。

相关代码

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();
    }
}

结论

请注意,必须将 binding 的生命周期所有者设置为 binding.setLifecycleOwner(this)MainViewModel 也必须为 binding 设置。否则,它将无法正常工作。如有疑问,请务必查看官方文档 here.

来自官方文档:

/**
 * 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

所需步骤:

  1. 确保在初始化视图绑定后设置activityOrFragmentBinding.lifecycleOwner = thisactivityOrFragmentBinding 将其替换为您的绑定变量名称。
  2. 始终将 ViewModel 设置为数据绑定。

这可能会对某人有所帮助....

在 A 模型视图中观察到 MediatorLiveData() 后,我希望设置 A Textviews 文本,事实证明 ObservableField() 非常完美,它的工作方式如下:-

        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)

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

我不确定这是否是您问题的答案,但正在更改

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

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

帮了我大忙。根据官方文档,在片段中初始化 ViewModel 时,需要使用 requireActivity()

不久前我也遇到了类似的问题。我在导航图中有 2 个片段,第二个片段恰好是第一个片段中某个项目的详细信息,ViewModel class 没有更新,即使我的调试日志显示正在设置数据.

事实证明,每次我调用 ViewModelProviders.of(this).get(MainViewModel.class); 时都会创建一个新的 ViewModel 实例。

解决方案是使 ViewModel 成为单例,尽管这可能不是最理想的解决方案。

在片段中,我只是将绑定生命周期所有者设置为该片段的生命周期所有者,例如:

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