【问题标题】:MutableLiveData not updating in UIMutableLiveData 未在 UI 中更新
【发布时间】:2019-12-31 14:15:34
【问题描述】:

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

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

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

我认为我不需要为片段中的值设置任何观察者,因为我在 XML 和 ViewModel 之间有 2 路数据绑定。

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。

【问题讨论】:

    标签: android android-livedata mutablelivedata


    【解决方案1】:

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

    您可能没有为您的 binding 对象设置生命周期所有者。从JAVADOCsetLifecycleOwner() 这个假设听起来更有说服力(请看here):

    如果 LiveData 在其中一个绑定表达式中并且没有 设置了 LifecycleOwner,LiveData 将不会被观察和更新 它不会传播到 UI。

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

    观察

    我注意到您没有展示您的 ViewModel 和绑定是如何创建的。因此,我创建了一个满足您要求并正确更新 UI 的简单项目 - LiveData 中的 setValuepostValue 正在无缝更改相应的 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>
    

    主片段

    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

    【讨论】:

    • 解决了我的问题!在片段中使用 kotlin + ktx 可以:binding = Fragment&lt;NAME&gt;Binding.inflate(inflater) 后跟 binding.lifecycleOwner = viewLifecycleOwner
    【解决方案2】:

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

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

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

    为我做了诀窍。根据官方文档,在片段中初始化 ViewModel 时,需要使用 requireActivity()

    【讨论】:

    • 这对我有帮助。谢谢你。在片段中,ViewModelProvider 需要 requireActivity() 而不是 'this'。
    【解决方案3】:

    来自官方文档:

    /**
     * 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. 确保在 intilizing 视图绑定后设置activityOrFragmentBinding.lifecycleOwner = thisactivityOrFragmentBinding 将其替换为您的绑定变量名称。
    2. 始终将 ViewModel 设置为数据绑定。

    【讨论】:

      【解决方案4】:

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

      根据documentation

      在大多数情况下,应用组件的 onCreate() 方法是正确的位置 开始观察 LiveData 对象

      对于this article末尾的片段

      编辑(2018 年 5 月 14 日):Google 决定直接在 支持库 28.0.0 和 AndroidX 1.0.0。我现在建议你 使用返回的特殊生命周期注册您的观察者 getViewLifecycleOwner()onCreateView()。我喜欢认为这 文章在 Google 解决此问题并获取 建议的解决方案。

      onCreateView() 是开始观察实时数据的正确位置。

      因此无需每次在片段的onResume 中观察实时数据。

      【讨论】:

        【解决方案5】:

        我不喜欢这个答案,但这是我目前唯一的答案。如果/当它出现时,我仍然会接受使用 2 路数据绑定的正确答案。

        在此之前,如果您遇到 2 路数据绑定更新您的 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);
            });
        
            ....
        }
        

        【讨论】:

          【解决方案6】:

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

          在 A 模型视图中观察到 MediatorLiveData() 后,我希望从 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>
          

          【讨论】:

            【解决方案7】:

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

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

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

            【讨论】:

              【解决方案8】:

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

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

              【讨论】:

                猜你喜欢
                • 2023-03-10
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多