Che*_*eng 41 android android-livedata android-architecture-components
我的理解LiveData
是,它将触发观察者对当前状态变化的数据,而不是一系列历史状态变化的数据.
目前,我有一个MainFragment
执行Room
写操作的程序,用于将非删除数据更改为已删除的数据.
我也是另一个TrashFragment
观察到破坏数据的人.
请考虑以下情形.
MainFragment
是当前活动的片段.TrashFragment
尚未创建.MainFragment
添加了1个已删除的数据.MainFragment
用TrashFragment
.TrashFragment
的观察者将首先收到onChanged
0个已删除的数据TrashFragment
观察者将接下来接收onChanged
1个已删除的数据 我的期望是,第(6)项不应该发生.TrashFragment
应该只接收最新的已删除数据,即1.
这是我的代码
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);
Run Code Online (Sandbox Code Playgroud)
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);
Run Code Online (Sandbox Code Playgroud)
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();
}
}
Run Code Online (Sandbox Code Playgroud)
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;
}
}
Run Code Online (Sandbox Code Playgroud)
onChanged
对于同样的数据,我知道如何防止两次接收?
我创建了一个演示项目来演示这个问题.
正如你所看到的,我执行写操作后(点击ADD丢弃注按钮)中MainFragment
,当我切换到TrashFragment
,我希望onChanged
在TrashFragment
将只被调用一次.然而,它被称为两次.
R. *_*ski 34
我在您的代码中只介绍了一个更改:
noteViewModel = ViewModelProviders.of(this).get(NoteViewModel.class);
Run Code Online (Sandbox Code Playgroud)
代替:
noteViewModel = ViewModelProviders.of(getActivity()).get(NoteViewModel.class);
Run Code Online (Sandbox Code Playgroud)
用Fragment
的onCreate(Bundle)
方法.现在它无缝地工作.
在您的版本中,您获得了NoteViewModel
两个片段(来自Activity)的共同参考.我认为,ViewModel
已经Observer
在之前的片段中注册过了.因此,LiveData
保持引用两个Observer
(in MainFragment
和TrashFragment
)并调用两个值.
所以我猜结论可能是,你应该ViewModel
从以下ViewModelProviders
来获得:
Fragment
在 Fragment
Activity
在 Activity
顺便说一句.
noteViewModel.getTrashedNotesLiveData().removeObservers(this);
在片段中没有必要,但我建议把它放进去onStop
.
Vas*_*liy 16
我分叉你的项目并测试了一下.我可以告诉你发现了一个严重的错误.
为了使复制和调查更容易,我对您的项目进行了一些编辑.您可以在此处找到更新的项目:https://github.com/techyourchance/live-data-problem.我还向你的回购开了一个拉请求.
为了确保不会忽视这一点,我还在Google的问题跟踪器中打开了一个问题:
重现步骤:
- 确保在MainFragment中将REPRODUCE_BUG设置为true
- 安装应用程序
- 点击"添加删除的笔记"按钮
- 切换到TrashFragment
- 请注意,只有一个通知形式的LiveData具有正确的值
- 切换到MainFragment
- 点击"添加删除的笔记"按钮
- 切换到TrashFragment
- 请注意,LiveData有两个通知,第一个通知值不正确
请注意,如果将REPRODUCE_BUG设置为false,则该错误不会重现.它演示了在MainFragment中订阅LiveData改变了TrashFragment中的行为.
预期结果:在任何情况下,只有一个通知具有正确的值.由于先前的订阅,行为没有变化.
更多信息:我查看了一些消息来源,看起来由于LiveData激活和新的Observer订阅而触发了通知.可能与ComputableLiveData将onActive()计算卸载到Executor的方式有关.
小智 16
原因是在您的.observe()方法中,您传递了一个片段作为生命周期所有者。应该传的是viewLifecycleOwner
fragment的对象
viewModel.livedata.observe(viewLifecycleOwner, Observer {
// Do your routine here
})
Run Code Online (Sandbox Code Playgroud)
我的回答不是这个问题描述的解决方案,而是问题标题的解决方案。只是标题。
如果您的 LiveData<*> 观察者被多次调用,则意味着您多次调用 livedata.observe(...) 。这发生在我身上,因为我在一个方法中执行 livedata.observe(...) ,并且每当用户执行某些操作时就会调用此方法,从而再次观察 liveData。为了解决这个问题,我将 livedata.observe(...) 移至 onCreate() 生命周期方法。
当时的情景是怎样的?
该应用程序有一个颜色样本。当用户选择一种颜色时,我必须调用 API 来获取该颜色的产品图像。进行 API 调用并观察onColorChanged()
. 当用户选择新颜色时,onColorChanged()
将再次调用,从而再次观察实时数据变化。
编辑:另一个问题可能是在注册 LiveData Observer 时传递此参数而不是viewLifecycleOwner,如下面另一个答案中所指出的。观察 Fragments 中的 LiveData 时,始终使用viewLifecycleOwner 。
我抓住了Vasiliy的叉子叉子并做了一些实际的调试,看看会发生什么.
可能与ComputableLiveData将onActive()计算卸载到Executor的方式有关.
关.Room的LiveData<List<T>>
曝光工作方式是它创建一个ComputableLiveData
,它可以跟踪你的数据集是否在Room下面无效.
trashedNotesLiveData = NoteplusRoomDatabase.instance().noteDao().getTrashedNotes();
Run Code Online (Sandbox Code Playgroud)
因此,当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);
}
Run Code Online (Sandbox Code Playgroud)
现在,我们需要知道的是,ComputableLiveData
的invalidate()
将实际刷新数据集,如果LiveData是积极的.
// 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);
}
}
}
};
Run Code Online (Sandbox Code Playgroud)
在哪里liveData.hasActiveObservers()
:
public boolean hasActiveObservers() {
return mActiveCount > 0;
}
Run Code Online (Sandbox Code Playgroud)
所以refreshRunnable
实际上只有在有活跃的观察者时才会运行(afaik意味着生命周期至少开始,并观察实时数据).
这意味着当您在TrashFragment中订阅时,会发生的情况是您的LiveData存储在Activity中,因此即使TrashFragment消失,它也会保持活动状态,并保留以前的值.
但是,当您打开TrashFragment,然后TrashFragment订阅,LiveData变为活动状态,ComputableLiveData检查失效(由于实时数据未处于活动状态而从未重新计算,这是真的),在后台线程上异步计算它,当它是完成后,该值已过帐.
所以你得到两个回调因为:
1.)首先"onChanged"调用是Activity的ViewModel中保存的LiveData的先前保留值
2.)第二个"onChanged"调用是来自数据库的新评估结果集,其中计算是由来自Room的实时数据变为活动触发的.
从技术上讲,这是设计的.如果您想确保只获得"最新且最好"的值,那么您应该使用片段范围的ViewModel.
您可能还想开始观察onCreateView()
并使用viewLifecycle
LiveData的生命周期(这是一个新的添加,因此您不需要删除观察者onDestroyView()
.
如果片段即使在片段未处于活动状态并且未观察到片段时看到最新值也很重要,那么当ViewModel是活动范围时,您可能希望在Activity中注册观察者以确保存在LiveData上的活跃观察者.
切勿将观察者放入循环/任何被注册两次的地方。观察者应该放在 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
}
);
}
Run Code Online (Sandbox Code Playgroud)
将其放在某个被多次调用的函数下是错误的,例如:
@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
}
);
}
Run Code Online (Sandbox Code Playgroud)
观察者方法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)
。这LiveData::considerNotify(ObserverWrapper observer)
将以观察者包装器作为参数进行调用。最终,调用者onChanged()
以加载的数据作为参数。
现在是第一个电话,您的要点6。
您可以在onCreateView()
hook方法中设置观察者。在这一点之后,生命周期将其状态更改为两次,on start
然后变为可见on resume
。内部类LiveData::LifecycleBoundObserver
会在状态发生变化时收到通知,因为它实现了GenericLifecycleObserver
接口,该接口包含一个名为的方法void onStateChanged(LifecycleOwner source, Lifecycle.Event event);
。
此方法ObserverWrapper::activeStateChanged(boolean newActive)
称为LifecycleBoundObserver
extends ObserverWrapper
。该方法activeStateChanged
调用dispatchingValue()
,然后LiveData::considerNotify(ObserverWrapper observer)
以观察者包装器作为参数进行调用。最后onChanged()
,这要求观察者。
所有这些都是在某些条件下发生的。我承认我没有调查方法链中的所有条件。状态有两种变化,但onChanged()
仅触发一次,因为条件会检查这种情况。
这里的底线是,存在一系列方法,这些方法是在生命周期更改时触发的。这负责第一次通话。
我认为您的代码没有错。很好,观察者是在创建时被调用的。因此它可以用视图模型的初始数据填充自身。即使在第一次通知时视图模型的数据库部分仍然为空,这也是观察者应该做的。
第一个通知基本上告诉我们视图模型已准备好显示,尽管该视图模型尚未从底层数据库中加载数据。第二个通知表明此数据已准备就绪。
当您想到数据库连接缓慢时,这是一种合理的方法。您可能想要从通知触发的视图模型中检索和显示其他数据,这些数据不是来自数据库。
Android提供了有关如何处理缓慢的数据库加载的指南。他们建议使用占位符。在此示例中,差距是如此之短,因此没有理由进行这种扩展。
两个片段都使用自己的ComputableLiveData
对象,这就是第二个对象没有从第一个片段预加载的原因。
还要考虑旋转的情况。视图模型的数据不变。它不会触发通知。仅生命周期的状态更改会触发新新视图的通知。
这是幕后发生的事情:
ViewModelProviders.of(getActivity())
Run Code Online (Sandbox Code Playgroud)
当您使用getActivity() 时,这会保留您的 NoteViewModel,而 MainActivity 的范围是活动的,因此您的trashedNotesLiveData 也是活动的。
当您第一次打开 TrashFragment 房间时查询数据库,并且您的trashedNotesLiveData 填充了垃圾值(在第一次打开时只有一个 onChange() 调用)。所以这个值缓存在trashingNotesLiveData 中。
然后你来到主片段添加一些垃圾笔记并再次转到 TrashFragment。这一次,当 room 进行异步查询时,您首先会使用trashedNotesLiveData 中的缓存值。查询完成后,您将获得最新值。这就是为什么您会收到两个 onChange() 调用。
所以解决方案是你需要在打开 TrashFragment 之前清理trashedNotesLiveData。这可以在您的 getTrashedNotesLiveData() 方法中完成。
public LiveData<List<Note>> getTrashedNotesLiveData() {
return NoteplusRoomDatabase.instance().noteDao().getTrashedNotes();
}
Run Code Online (Sandbox Code Playgroud)
或者你可以使用这样的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);
}
}
});
Run Code Online (Sandbox Code Playgroud)
归档时间: |
|
查看次数: |
27389 次 |
最近记录: |