为什么要为新连接的观察者

时间:2018-05-08 15:05:30

标签: android android-architecture-components android-livedata

我对LiveData的理解是,它会触发观察者对数据的当前状态变化,而不是一系列历史状态的数据变化。

目前,我有MainFragment执行Room写入操作,将非删除数据更改为已删除数据。< / p>

我还有另一个TrashFragment,它会观察已删除的数据

请考虑以下情况。

  1. 目前有0 已删除数据
  2. MainFragment是当前活动的片段。尚未创建TrashFragment
  3. MainFragment添加了1个已删除的数据
  4. 现在,有1个已删除的数据
  5. 我们使用导航抽屉将MainFragment替换为TrashFragment
  6. TrashFragment观察员将首先收到onChanged,其中包含0 已删除的数据
  7. 同样,TrashFragment的观察者将第二次收到onChanged,其中包含1个已删除的数据
  8. 我的期望是,第(6)项不应该发生。 TrashFragment只应收到最新的已删除数据,即1。

    这是我的代码

    TrashFragment.java

    public class TrashFragment extends Fragment {
        @Override
        public void onCreate(Bundle savedInstanceState) {
            noteViewModel = ViewModelProviders.of(getActivity()).get(NoteViewModel.class);
        }
    
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            ...
    
            noteViewModel.getTrashedNotesLiveData().removeObservers(this);
            noteViewModel.getTrashedNotesLiveData().observe(this, notesObserver);
    

    MainFragment.java

    public class MainFragment extends Fragment {
        @Override
        public void onCreate(Bundle savedInstanceState) {
            noteViewModel = ViewModelProviders.of(getActivity()).get(NoteViewModel.class);
        }
    
        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            ...
    
            noteViewModel.getNotesLiveData().removeObservers(this);
            noteViewModel.getNotesLiveData().observe(this, notesObserver);
    

    NoteViewModel .java

    public class NoteViewModel extends ViewModel {
        private final LiveData<List<Note>> notesLiveData;
        private final LiveData<List<Note>> trashedNotesLiveData;
    
        public LiveData<List<Note>> getNotesLiveData() {
            return notesLiveData;
        }
    
        public LiveData<List<Note>> getTrashedNotesLiveData() {
            return trashedNotesLiveData;
        }
    
        public NoteViewModel() {
            notesLiveData = NoteplusRoomDatabase.instance().noteDao().getNotes();
            trashedNotesLiveData = NoteplusRoomDatabase.instance().noteDao().getTrashedNotes();
        }
    }
    

    处理房间的代码

    public enum NoteRepository {
        INSTANCE;
    
        public LiveData<List<Note>> getTrashedNotes() {
            NoteDao noteDao = NoteplusRoomDatabase.instance().noteDao();
            return noteDao.getTrashedNotes();
        }
    
        public LiveData<List<Note>> getNotes() {
            NoteDao noteDao = NoteplusRoomDatabase.instance().noteDao();
            return noteDao.getNotes();
        }
    }
    
    @Dao
    public abstract class NoteDao {
        @Transaction
        @Query("SELECT * FROM note where trashed = 0")
        public abstract LiveData<List<Note>> getNotes();
    
        @Transaction
        @Query("SELECT * FROM note where trashed = 1")
        public abstract LiveData<List<Note>> getTrashedNotes();
    
        @Insert(onConflict = OnConflictStrategy.REPLACE)
        public abstract long insert(Note note);
    }
    
    @Database(
            entities = {Note.class},
            version = 1
    )
    public abstract class NoteplusRoomDatabase extends RoomDatabase {
        private volatile static NoteplusRoomDatabase INSTANCE;
    
        private static final String NAME = "noteplus";
    
        public abstract NoteDao noteDao();
    
        public static NoteplusRoomDatabase instance() {
            if (INSTANCE == null) {
                synchronized (NoteplusRoomDatabase.class) {
                    if (INSTANCE == null) {
                        INSTANCE = Room.databaseBuilder(
                                NoteplusApplication.instance(),
                                NoteplusRoomDatabase.class,
                                NAME
                        ).build();
                    }
                }
            }
    
            return INSTANCE;
        }
    }
    

    任何想法如何阻止两次接收onChanged相同的数据?

    演示

    我创建了一个演示项目来演示这个问题。

    正如您所看到的,在MainFragment中执行写入操作(点击添加删除注释按钮)后,当我切换到TrashFragment时,我希望{{1 } onChanged只会被调用一次。然而,它被召唤两次。

    enter image description here

    可以从https://github.com/yccheok/live-data-problem

    下载演示项目

13 个答案:

答案 0 :(得分:23)

我在您的代码中只引入了一项更改:

noteViewModel = ViewModelProviders.of(this).get(NoteViewModel.class);

而不是:

noteViewModel = ViewModelProviders.of(getActivity()).get(NoteViewModel.class);

Fragment onCreate(Bundle)方法。现在它无缝地工作。

在您的版本中,您获得了两个片段(来自活动)共有NoteViewModel的引用。我认为ViewModel已在先前的片段中注册了Observer。因此,LiveData始终引用Observer&(MainFragmentTrashFragment)并调用这两个值。

所以我猜结论可能是,你应该从ViewModel获得ViewModelProviders来自:

    Fragment
  • FragmentActivity
  • Activity

顺便说一下。

noteViewModel.getTrashedNotesLiveData().removeObservers(this);

片段中没有必要

,但我建议将其放入onStop

答案 1 :(得分:11)

我分叉你的项目并测试了一下。我可以告诉你发现了一个严重的错误。

为了使复制和调查更容易,我稍微编辑了你的项目。您可以在此处找到更新的项目:https://github.com/techyourchance/live-data-problem。我还向你的回购开了一个拉请求。

为了确保不会忽视这一点,我还在Google的问题跟踪器中opened an issue

  

重现的步骤:

     
      
  1. 确保在MainFragment
  2. 中将REPRODUCE_BUG设置为true   
  3. 安装应用
  4.   
  5. 点击“添加已删除的笔记”按钮
  6.   
  7. 切换到TrashFragment
  8.   
  9. 请注意,只有一个表单具有正确值的LiveData通知
  10.   
  11. 切换到MainFragment
  12.   
  13. 点击“添加已删除的笔记”按钮
  14.   
  15. 切换到TrashFragment
  16.   
  17. 请注意,LiveData有两个通知,第一个通知值不正确
  18.         

    请注意,如果将REPRODUCE_BUG设置为false,则错误不会   复制。它演示了对LiveData的订阅   MainFragment改变了TrashFragment中的行为。

         

    预期结果:在任何情况下,只有一个通知具有正确的值。   由于先前的订阅,行为没有变化。

         

    更多信息:我看了一下这些来源,看起来像   由于LiveData激活和新激活而触发通知   观察者订阅。可能与ComputableLiveData的方式有关   将onActive()计算卸载到Executor。

答案 2 :(得分:8)

我抓住了Vasiliy的叉子叉子并做了一些实际的调试,看看会发生什么。

  

可能与ComputableLiveData将onActive()计算卸载到Executor的方式有关。

关闭。 Room LiveData<List<T>>曝光的方式是它创建一个ComputableLiveData,用于跟踪您的数据集是否已在房间内无效。

trashedNotesLiveData = NoteplusRoomDatabase.instance().noteDao().getTrashedNotes();

因此,当写入note表时,绑定到LiveData的InvalidationTracker将在写入时调用invalidate()

  @Override
  public LiveData<List<Note>> getNotes() {
    final String _sql = "SELECT * FROM note where trashed = 0";
    final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
    return new ComputableLiveData<List<Note>>() {
      private Observer _observer;

      @Override
      protected List<Note> compute() {
        if (_observer == null) {
          _observer = new Observer("note") {
            @Override
            public void onInvalidated(@NonNull Set<String> tables) {
              invalidate();
            }
          };
          __db.getInvalidationTracker().addWeakObserver(_observer);
        }

现在我们需要知道的是,如果LiveData 处于活动状态,ComputableLiveData invalidate()实际刷新数据集

// invalidation check always happens on the main thread
@VisibleForTesting
final Runnable mInvalidationRunnable = new Runnable() {
    @MainThread
    @Override
    public void run() {
        boolean isActive = mLiveData.hasActiveObservers();
        if (mInvalid.compareAndSet(false, true)) {
            if (isActive) { // <-- this check here is what's causing you headaches
                mExecutor.execute(mRefreshRunnable);
            }
        }
    }
};

liveData.hasActiveObservers()的位置:

public boolean hasActiveObservers() {
    return mActiveCount > 0;
}

所以refreshRunnable实际上只有在有活跃的观察者时才会运行(afaik意味着生命周期至少开始,并观察实时数据)。

这意味着当您在TrashFragment中订阅时,会发生的情况是您的LiveData存储在Activity中,因此即使TrashFragment消失,它也会保持活动状态,并保留以前的值。

然而,当你打开TrashFragment,然后TrashFragment订阅,LiveData变为活动状态,ComputableLiveData检查失效(这是真的,因为它从未重新计算,因为实时数据不活动),在后台线程上异步计算它,并且完成后,将发布该值。

所以你得到两个回调因为:

1。)首先&#34; onChanged&#34; call是之前在Activity的ViewModel

中保留的LiveData保留值

2。)第二次&#34; onChanged&#34; call是数据库中新评估的结果集,其中计算是由Room的实时数据激活而触发的。

从技术上讲,这是设计的。如果你想确保你只获得最新和最好的&#34;值,那么你应该使用片段范围的ViewModel。

您可能还希望开始在onCreateView()中观察,并使用viewLifecycle作为LiveData的生命周期(这是一个新增加的内容,因此您无需在{{{}}中删除观察者{1}}。

如果Fragment看不到最新值,即使Fragment未处于活动状态并且没有观察它,那么当ViewModel是活动范围的时候,你可能也希望在Activity中注册一个观察者,以确保您的LiveData上有一个活跃的观察者。

答案 3 :(得分:6)

这不是一个错误,它是一个功能。了解原因!

观察者方法void onChanged(@Nullable T t)被调用两次。没关系。

第一次在启动时调用它。 Room第二次加载数据后立即调用它。因此,在第一次调用时,LiveData对象仍然是空的。它是出于这个原因而设计的。

第二次电话

让我们从第二个电话开始,您的观点7. Room的文档说:

  

Room生成更新LiveData对象所需的所有代码   何时更新数据库。生成的代码运行查询   在需要时在后台线程上异步。

生成的代码是其他帖子中提到的类ComputableLiveData的对象。它管理MutableLiveData对象。在此LiveData对象后,它会调用LiveData::postValue(T value),然后调用LiveData::setValue(T value)

LiveData::setValue(T value)来电LiveData::dispatchingValue(@Nullable ObserverWrapper initiator)。这将使用observer包装器作为参数调用LiveData::considerNotify(ObserverWrapper observer)。这最终在观察者上调用onChanged()并将加载的数据作为参数。

第一次致电

现在第一个电话,你的观点6.

您可以在onCreateView()挂钩方法中设置观察者。在此之后,生命周期会将其状态更改两次以显示,on starton resume。内部类LiveData::LifecycleBoundObserver会在状态更改时得到通知,因为它实现了GenericLifecycleObserver接口,该接口包含一个名为void onStateChanged(LifecycleOwner source, Lifecycle.Event event);的方法。

此方法调用ObserverWrapper::activeStateChanged(boolean newActive) LifecycleBoundObserver扩展ObserverWrapper。方法activeStateChanged调用dispatchingValue(),后者又以观察者包装器作为参数调用LiveData::considerNotify(ObserverWrapper observer)。这最终给观察者打了onChanged()

所有这些都在某些条件下发生。我承认我没有调查方法链中的所有条件。状态有两处变化,但onChanged()只触发一次,因为条件会检查这样的事情。

这里的底线是,有一系列方法,在生命周期发生变化时触发。这是第一次电话会议的责任。

底线

我认为您的代码没有任何问题。它很好,观察者被称为创造。因此它可以填充视图模型的初始数据。这是观察者应该做的事情,即使第一次通知时视图模型的数据库部分仍然是空的。

用法

第一个通知基本上告诉视图模型已准备好显示,尽管它仍未加载来自底层数据库的数据。第二个通知告诉我,这个数据准备好了。

当您考虑慢速数据库连接时,这是一种合理的方法。您可能希望从通知触发的视图模型中检索和显示其他数据,这些数据不是来自数据库。

Android有一个指导如何处理慢速数据库加载。他们建议使用占位符。在这个例子中,差距很小,没有理由这么做。

附录

两个片段都使用自己的ComputableLiveData个对象,这就是为什么第二个对象没有从第一个片段预加载的原因。

还要想一想轮换的情况。视图模型的数据不会更改。它不会触发通知。仅生命周期的状态更改会触发新视图的通知。

答案 4 :(得分:3)

原因是,在您的 .observe()方法中,您传递了一个片段作为生命周期所有者。应该传递的是片段的viewLifecycleOwner对象

viewModel.livedata.observe(viewLifecycleOwner, Observer {
        // Do your routine here
    })

答案 5 :(得分:2)

我的答案不是此问题描述的解决方案,而是问题标题的解决方案。只是标题。

如果您的LiveData观察者<*>被多次调用,则意味着您多次调用livedata.observe(...)。在我做livedata时,这发生在我身上.observe(...)在一个方法中,并在用户执行任何操作时调用此方法,从而再次观察liveData。为了解决这个问题,我将livedata.observe(...)移到了onCreate()生命周期方法中。

情况如何? 该应用程序有一个色板。当用户选择一种颜色时,我必须进行API调用以获取该颜色的产品图片。因此,进行API调用并观察onColorChanged()中的实时数据。当用户选择新颜色时,onColorChanged()将再次被调用,从而再次观察实时数据的变化。

答案 6 :(得分:1)

这就是幕后发生的事情:

ViewModelProviders.of(getActivity())

当您使用 getActivity()时,这将保留您的NoteViewModel,而MainActivity的范围仍然存在,因此是您的trashedNotesLiveData。

当您第一次打开TrashFragment会议室时,查询数据库并使用已删除的值填充您的trashedNotesLiveData(在第一次打开时,只有一个onChange()调用)。所以这个值缓存在trashedNotesLiveData中。

然后你来到主片段添加一些删除的注释并再次转到TrashFragment。这次您首先获得缓存值 trashedNotesLiveData,而房间进行异步查询。当查询完成时,你是 带来了最新的价值。这就是你得到两个onChange()调用的原因。

所以解决方案是你需要在打开之前清理trashedNotesLiveData TrashFragment。这可以在getTrashedNotesLiveData()方法中完成。

public LiveData<List<Note>> getTrashedNotesLiveData() {
    return NoteplusRoomDatabase.instance().noteDao().getTrashedNotes();
}

或者您可以使用类似SingleLiveEvent

之类的内容

或者您可以使用MediatorLiveData拦截Room生成的一个并仅返回不同的值。

final MediatorLiveData<T> distinctLiveData = new MediatorLiveData<>();
    distinctLiveData.addSource(liveData, new Observer<T>() {
        private boolean initialized = false;
        private T lastObject = null;

        @Override
        public void onChanged(@Nullable T t) {
            if (!initialized) {
                initialized = true;
                lastObject = t;
                distinctLiveData.postValue(lastObject);
            } else if (t != null && !t.equals(lastObject)) {
                lastObject = t;
                distinctLiveData.postValue(lastObject);
            }

        }
    });

答案 7 :(得分:1)

我特意发现了它为什么会按照它的方式行事。垃圾片段中的观察行为是onChanged(),一旦你在删除一个音符后(在新的应用程序启动时)激活片段就被调用,并且在片段被删除之后片段被激活之后被调用两次。

双重调用是因为:

调用#1:片段在其生命周期中在STOPPED和STARTED之间转换,这导致将通知设置为LiveData对象(毕竟它是生命周期观察者!)。 LiveData代码调用onChanged()处理程序,因为它认为观察者的数据版本需要更新(稍后会详细介绍)。注意:此时对数据的实际更新仍可能处于暂挂状态,从而导致使用陈旧数据调用onChange()。

调用#2:作为查询设置LiveData(正常路径)的结果。 LiveData对象再次认为观察者的数据版本是陈旧的。

现在为什么onChanged()只在应用启动后第一次激活视图时被称为一次?这是因为第一次LiveData版本检查代码由于STOPPED-&gt; STARTED转换而执行,实时数据从未被设置为任何内容,因此LiveData会跳过通知观察者。通过此代码路径进行的后续调用(请参阅LiveData.java中的considerationNotify())在数据至少设置一次后执行。

LiveData通过保留指示数据设置次数的版本号来确定观察者是否具有过时数据。它还记录上次发送给客户端的版本号。设置新数据时,LiveData可以比较这些版本以确定是否有必要进行onChange()调用。

这是在调用4个调用的LiveData版本检查代码期间的#s版本:

   Ver. Last Seen  Ver. of the     OnChanged()
   by Observer     LiveData        Called?
  --------------   --------------- -----------
1  -1 (never set)  -1 (never set)  N
2  -1              0               Y
3  -1              0               Y
4   0              1               Y

如果您想知道为什么调用3中观察者最后看到的版本为-1,即使onChanged()第二次被调用,因为调用1/2中的观察者是不同的观察者而不是调用3/4中的观察者(观察者在用户返回主片段时被破坏的片段中)。

避免因生命周期转换而发生的虚假调用混淆的一种简单方法是将片段中的标志初始化为false,以指示片段是否已完全恢复。在onResume()处理程序中将该标志设置为true,然后检查onChanged()处理程序中该标志是否为true。通过这种方式,您可以确保您对发生的事件做出了相应的响应,因为数据是真正设置的。

答案 8 :(得分:1)

我不确定这个问题是否仍然存在。

但是主要的肇事者是片段生命周期所有者内部的一个片段错误,该错误在视图被销毁时并未清除。

以前,您必须实现自己的lyfecycle所有者,该所有者将在调用destroyed时将状态移至onDestroyView

如果您至少要使用API​​ 28进行定位和编译,就不再是这种情况

答案 9 :(得分:1)

如果您正在寻找一种解决方案,以避免在popUp上使用多个触发器,则从目标片段到原始片段的后堆栈

我的解决方案是观察Fragment生命周期的onCreate()处的LiveData,并将生命周期所有者设为Activity ,并从以下位置删除观察者 片段生命周期的onDestroy()

答案 10 :(得分:1)

永远不要将观察者放在循环中/任何注册两次的地方。观察者应该放在 onViewCreated / onCreate / 任何只被调用一次的地方。只观察一次!

这是一个错误方式的例子:

for(int i=0;i<5;i++){
//THIS IS WRONG, DONT PUT IT INSIDE A LOOP / FUNCTION CALL
    yourviewModel.getYourLiveData().observe(getViewLifecycleOwner(), new Observer<Boolean>() {
            @Override
            public void onChanged(Boolean sBoolean) {
                 //SOME CODE 
            }
 );
}

将其置于某个被多次调用的函数之下是错误的,例如:

@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
observeMyViewModel();
observeMyViewModel();//THIS IS WRONG, CALLING IT MORE THAN ONCE
}

private void observeMyViewModel(){
  yourviewModel.getYourLiveData().observe(getViewLifecycleOwner(), new Observer<Boolean>() {
            @Override
            public void onChanged(Boolean sBoolean) {
                 //SOME CODE 
            }
 );
}

答案 11 :(得分:0)

我使用了SingleLiveEvent并且可以工作。恢复或重新创建片段/活动时,仅当显式更改时,SingleLiveEvent才抛出事件

答案 12 :(得分:-1)

我的解决方案只是在需要时开始观察数据,并在检索到数据后立即删除观察者。这样您不会双重触发。