LiveData 在第一次回调后删除观察者

Posted

技术标签:

【中文标题】LiveData 在第一次回调后删除观察者【英文标题】:LiveData remove Observer after first callback 【发布时间】:2018-05-30 23:27:38 【问题描述】:

收到第一个结果后如何移除观察者?以下是我尝试过的两种代码方式,但即使我删除了观察者,它们都会继续接收更新。

Observer observer = new Observer<DownloadItem>() 
        @Override
        public void onChanged(@Nullable DownloadItem downloadItem) 
            if(downloadItem!= null) 
                DownloadManager.this.downloadManagerListener.onDownloadManagerFailed(null, "this item already exists");
                return;
            
            startDownload();
            model.getDownloadByContentId(contentId).removeObservers((AppCompatActivity)context);
        
    ;
    model.getDownloadByContentId(contentId).observeForever(observer);

 model.getDownloadByContentId(contentId).observe((AppCompatActivity)context, downloadItem-> 
             if(downloadItem!= null) 
                this.downloadManagerListener.onDownloadManagerFailed(null, "this item already exists");
                return;
            
            startDownload();
            model.getDownloadByContentId(contentId).removeObserver(downloadItem-> );
         );

【问题讨论】:

【参考方案1】:

有一个更方便的 Kotlin 扩展解决方案:

fun <T> LiveData<T>.observeOnce(lifecycleOwner: LifecycleOwner, observer: Observer<T>) 
    observe(lifecycleOwner, object : Observer<T> 
        override fun onChanged(t: T?) 
            observer.onChanged(t)
            removeObserver(this)
        
    )

这个扩展允许我们这样做:

liveData.observeOnce(this, Observer<Password> 
    if (it != null) 
        // do something
    
)

所以要回答你原来的问题,我们可以这样做:

val livedata = model.getDownloadByContentId(contentId)
livedata.observeOnce((AppCompatActivity) context, Observer<T> 
    if (it != null) 
        DownloadManager.this.downloadManagerListener.onDownloadManagerFailed(null, "this item already exists");
    
    startDownload();
)

原文出处:https://code.luasoftware.com/tutorials/android/android-livedata-observe-once-only-kotlin/

更新:@Hakem-Zaied 是对的,我们需要使用 observe 而不是 observeForever

【讨论】:

我可以建议先删除观察者 - 只有然后调用用户的onChanged。否则,如果用户的实现抛出异常,中间观察者将“永远”保持注册状态。 如果在哪里声明扩展函数不明显,请参阅:kotlinlang.org/docs/reference/… 很棒的答案!我稍微修改了一下,现在它更像 Kotlinish 了! gist.github.com/bartekpacia/eb1c92886acf3972c3f030cde2579ebb 这段代码有问题。您需要添加检查以查看生命周期所有者是否已销毁,否则可能会导致泄漏和尝试调用已销毁片段的方法或属性的问题。您应该编辑它并将其添加到 onChanged: if (owner.lifecycle.currentState == Lifecycle.State.DESTROYED) removeObserver(this) return @paul_f 我不这么认为,如果所有者已经被销毁,那么observe 将忽略该调用。查看来源here【参考方案2】:

您的第一个将不起作用,因为observeForever() 未绑定到任何LifecycleOwner

您的第二个将不起作用,因为您没有将现有的注册观察者传递给removeObserver()

您首先需要确定是否将LiveDataLifecycleOwner(您的活动)一起使用。我的假设是您应该使用LifecycleOwner。在这种情况下,请使用:

Observer observer = new Observer<DownloadItem>() 
    @Override
    public void onChanged(@Nullable DownloadItem downloadItem) 
        if(downloadItem!= null) 
            DownloadManager.this.downloadManagerListener.onDownloadManagerFailed(null, "this item already exists");
            return;
        
        startDownload();
        model.getDownloadByContentId(contentId).removeObservers((AppCompatActivity)context);
    
;

model.getDownloadByContentId(contentId).observe((AppCompatActivity)context, observer);

【讨论】:

这将从活动/片段中删除所有观察者。我们如何才能只删除当前的观察者,而不是全部? @MrVasilev:我不知道“当前”在这种情况下是什么意思。要删除单个观察者,请在 LiveData 上调用 removeObserver() @CommonsWare 对不起,如果我不清楚。当我尝试这样做时: var liveData = viewModel.findNearestDriver(location) liveData.observe(this, Observer liveData.removeObserver(this) ) 我收到“类型不匹配”编译错误,因为这不是我的观察者,而是我的片段 @MrVasilev:这可能是 Kotlin SAM 对 lambdas 或其他东西的支持的问题。您可能需要使用object : Observer 并创建一个“真实的”Observer 实例,以便从ObserveronChanged() 函数内部获取正确的this @CommonsWare 感谢您的回复,您说得对,这是 Kotlin 的问题。只需提一下,对于 Kotlin,解决方案可能是像这样创建“LiveData”的扩展: fun LiveData.observeOnlyOnce(lifecycleOwner: LifecycleOwner, observer: Observer) observe(lifecycleOwner, object : Observer override fun onChanged(t: T?) observer.onChanged(t) removeObserver(this) ) 【参考方案3】:

按照CommonsWare 的回答,您可以简单地调用removeObserver(this) 来仅删除该观察者,而不是调用removeObservers() 来删除所有附加到LiveData 的观察者:

Observer observer = new Observer<DownloadItem>() 
    @Override
    public void onChanged(@Nullable DownloadItem downloadItem) 
        if(downloadItem!= null) 
            DownloadManager.this.downloadManagerListener.onDownloadManagerFailed(null, "this item already exists");
            return;
        
        startDownload();
        model.getDownloadByContentId(contentId).removeObserver(this);
    
;

model.getDownloadByContentId(contentId).observe((AppCompatActivity)context, observer);

注意:removeObserver(this) 中,this 指的是观察者实例,这仅适用于匿名内部类的情况。如果您使用 lambda,则 this 将引用活动实例。

【讨论】:

这里要小心...根据您的查询返回的速度,这实际上会导致一个无限循环,其中 model.myThing 在调用 removeObserver 之前返回(我遇到过这种情况)。 @Psest328 这究竟是如何导致循环的? 我说LiveData似乎没有没有参数的方法removeObservers()是不是错了?在不知道生命周期所有者的情况下,我似乎无法找到删除所有观察者的方法。 @Nick 您正在调用该方法并在调用删除观察者的过程中将其启动。如果该过程在观察者被移除之前完成,则会导致无限循环。你基本上是在创造一个竞争条件 @Psest328 "观察者被移除之前" 观察者被同步移除...【参考方案4】:

我喜欢 Vince 和 Hakem Zaied 的通用解决方案,但对我来说,lambda 版本似乎更好:

fun <T> LiveData<T>.observeOnce(observer: (T) -> Unit) 
    observeForever(object: Observer<T> 
        override fun onChanged(value: T) 
            removeObserver(this)
            observer(value)
        
    )


fun <T> LiveData<T>.observeOnce(owner: LifecycleOwner, observer: (T) -> Unit) 
    observe(owner, object: Observer<T> 
        override fun onChanged(value: T) 
            removeObserver(this)
            observer(value)
        
    )

所以你最终得到:

    val livedata = model.getDownloadByContentId(contentId)
    livedata.observeOnce((AppCompatActivity) context) 
        if (it != null) 
            DownloadManager.this.downloadManagerListener.onDownloadManagerFailed(null, "this item already exists")
        
        startDownload();
    

我觉得更干净。

另外,removeObserver() 在观察者被调度时被称为第一件事,这使得它更安全(即处理用户观察者代码中潜在的运行时错误抛出)。

【讨论】:

Kotlin 用户的完美答案!我只是写了相同的代码并想发布它,直到我找到它。 +1 这段代码有问题。您需要添加检查以查看生命周期所有者是否已销毁,否则可能会导致泄漏和尝试调用已销毁片段的方法或属性的问题。您应该编辑它并将其添加到 onChanged: if (owner.lifecycle.currentState == Lifecycle.State.DESTROYED) removeObserver(this) return 如果我在 oncreateview 中使用它,它会在我回到片段时触发,有什么建议我应该在哪个生命周期方法中使用它? 谢谢。这很好地缓解了问题。【参考方案5】:

我同意上面的Vince,但我相信我们要么跳过lifecycleOwner,而是使用observerForever,如下所示:

fun <T> LiveData<T>.observeOnce(observer: Observer<T>) 
    observeForever(object : Observer<T> 
        override fun onChanged(t: T?) 
            observer.onChanged(t)
            removeObserver(this)
        
    )

或者,使用lifecycleOwnerobserve 如下:

fun <T> LiveData<T>.observeOnce(lifecycleOwner: LifecycleOwner, observer: Observer<T>) 
    observe(lifecycleOwner, object : Observer<T> 
        override fun onChanged(t: T?) 
            observer.onChanged(t)
            removeObserver(this)
        
    )

【讨论】:

您可以使用 Java 发布相同的答案吗?我对 Kotlin 的了解为零。 这段代码有问题。您需要添加检查以查看生命周期所有者是否已销毁,否则可能会导致泄漏和尝试调用已销毁片段的方法或属性的问题。您应该编辑它并将其添加到 onChanged: if (owner.lifecycle.currentState == Lifecycle.State.DESTROYED) removeObserver(this) return 谢谢,它有效,但在所有情况下都取消订阅。例如,您加载一个列表,具有三种状态:加载、成功、错误。然后你只会看到加载状态。要修复它,请在完成状态(成功、错误)后取消订阅。【参考方案6】:

这是其他答案中建议的 observeOnce 方法的 Java 版本(一个 util 类方法而不是 Kotlin 扩展函数):

public class LiveDataUtil 

    public static <T> void observeOnce(final LiveData<T> liveData, final Observer<T> observer) 
        liveData.observeForever(new Observer<T>() 
            @Override
            public void onChanged(T t) 
                liveData.removeObserver(this);
                observer.onChanged(t);
            
        );
    


【讨论】:

【参考方案7】:

Java 版本的observeOnce 方法已经被许多用户推荐。但这里我们将在主代码中实现。

首先,我们需要创建Util类方法

public class LiveDataUtil 
public static <T> void observeOnce(final LiveData<T> liveData, final Observer<T> observer) 
    liveData.observeForever(new Observer<T>() 
        @Override
        public void onChanged(T t) 
            liveData.removeObserver(this);
            observer.onChanged(t);
        
    );

现在,我们需要在需要 ViewModel 的地方调用这个类。

LiveDataUtil.observeOnce(viewModel.getUserDetails(), response-> 
    if(response.isSuccessful())
       //Do your task
    
 

就是这样!

【讨论】:

【参考方案8】:

您不止一次地创建实时数据实例 (model.getDownloadByContentId(contentId)),这是这里的问题。

试试这个:

LiveData myLiveData =model.getDownloadByContentId(contentId);
myLiveData.observe(getViewLifecycleOwner(), downloadItem-> 
         if(downloadItem!= null) 
            this.downloadManagerListener.onDownloadManagerFailed(null, "this item already exists");
            return;
        
        startDownload();
       myLiveData.removeObservers(getViewLifecycleOwner());
     );

【讨论】:

【参考方案9】:

@CommonsWare 和@Toni Joe 提出的解决方案没有为我解决问题,因为我在 ViewModel 中收到 DAO 查询的第一个结果后需要删除观察者。但是,在Livedata keeps observer after calling removeObserer 找到的以下解决方案以我自己的一点直觉为我解决了问题。

过程如下,根据请求在存储LiveData的ViewModel中创建一个变量,在进行null检查后在activity中的create observer函数调用中检索它,并在调用flushToDB之前调用remove observers函数导入类中的例程。也就是说,我的 ViewModel 中的代码如下所示:

public class GameDataModel extends AndroidViewModel 
   private LiveData<Integer> lastMatchNum = null;
   .
   .
   .
   private void initLastMatchNum(Integer player1ID, Integer player2ID) 
       List<Integer> playerIDs = new ArrayList<>();
       playerIDs.add(player1ID);
       playerIDs.add(player2ID);

       lastMatchNum = mRepository.getLastMatchNum(playerIDs);
   

 public LiveData<Integer> getLastMatchNum(Integer player1ID, Integer player2ID) 
       if (lastMatchNum == null)  initLastMatchNum(player1ID, player2ID); 
       return lastMatchNum;
   

在上面,如果 ViewModel 中的 LiveData 变量中没有数据,我会调用 initLastMatchNum() 从视图模型中的函数中检索数据。要从活动中调用的函数是getLastMatchNum()。该例程检索 ViewModel 中变量中的数据(通过 DAO 通过存储库检索)。

我的活动中有以下代码

public class SomeActivity extends AppCompatActivity 

    @Override
    protected void onCreate(Bundle savedInstanceState) 
         .
         .
         .
        setupLastMatchNumObserver(); 
         .
         .
         .
    

    private void setupLastMatchNumObserver() 
        if (mGameDataViewModel.getLastMatchNum(Player1ID, Player2ID).hasObservers()) 
            Log.v("Observers", "setupLastMatchNumObserver has observers...returning");
            return;
        
        Log.v("Setting up Observers", "running mGameDataViewModel.get...observer()");
        mGameDataViewModel.getLastMatchNum(Player1ID, Player2ID).observe(this, new Observer<Integer>() 
            @Override
            public void onChanged(Integer MatchNumber) 
                if (MatchNumber == null ) 
                    matchNumber = 1;
                    Log.v( "null MatchNumber", "matchNumber: " + matchNumber.toString());
                
                else 
                    matchNumber = MatchNumber; matchNumber++;
                    Log.v( "matchNumber", "Incrementing match number: " + matchNumber.toString());
                
                MatchNumberText.setText(matchNumber.toString());
            
        );
    

    private void removeObservers() 
        final LiveData<Integer> observable = mGameDataViewModel.getLastMatchNum(Player1ID, Player2ID);
        if (observable != null && observable.hasObservers()) 
            Log.v("removeObserver", "Removing Observers");
            observable.removeObservers(this);
        
    

上面发生的事情是 1.) 我在活动的onCreate 方法中调用setupLastMatchNumObserver() 例程,以更新类的变量matchNum。这会跟踪存储在数据库中的游戏中玩家之间的匹配数。每组球员在数据库中都会有不同的比赛号码,这取决于他们彼此进行新比赛的频率。这个线程中的第一个解决方案对我来说似乎有点厌倦,因为在onChanged 中调用删除观察者对我来说似乎很奇怪,并且会在玩家每次移动的每次数据库刷新后不断更改TextView 对象。所以matchNumber 在每次移动后都会增加,因为在第一次移动之后数据库中有一个新值(即matchNumber++ 值),并且onChanged 一直被调用,因为removeObservers 没有按预期工作。 setupLastMatchNumObserver() 检查是否有实时数据的观察者,如果有,则不会在每一轮实例化一个新调用。如您所见,我正在设置一个TextView 对象来反映玩家的当前比赛编号。

下一部分是关于何时调用removeObservers() 的小技巧。起初我想如果我在 setupLastMatchNumObserver() 之后直接调用它,在活动的 onCreate 覆盖中,一切都会好起来的。但它在观察者获取数据之前移除了观察者。我发现如果我在调用之前直接调用removeObservers() 将活动中收集的新数据刷新到数据库(在整个活动中的单独例程中),它就像一个魅力。即,

 public void addListenerOnButton() 
        .
        .
        .
            @Override
            public void onClick(View v) 
               .
               .
               .
               removeObservers();
               updateMatchData(data); 
            
   

我也以上述方式在我的活动中的其他地方调用removeObservers();updateMatchData(data)。美妙之处在于removeObservers() 可以根据需要多次调用,因为如果没有观察者在场,则会返回检查。

【讨论】:

【参考方案10】:
    LiveData 类有 2 个类似的方法来删除观察者。首先是命名,

removeObserver(@NonNull final Observer&lt;T&gt; observer)(仔细看方法的名称,它是单数的)它接收你想要从同一个 LifecycleOwner 的观察者列表中删除的观察者。

    第二种方法是

removeObservers(@NonNull final LifecycleOwner owner)(见复数方法名)。该方法接受 LifecycleOwner 本身,并移除指定 LifecycleOwner 的所有 Observer。

现在,在您的情况下,您可以通过 2 种方式删除您的观察者(可能有多种方式),@ToniJoe 在上一个答案中告诉您。

另一种方法是在您的 ViewModel 中添加一个 MutableLiveData 布尔值,它在第一次被观察时存储 true,并且也只观察该 Livedata。因此,只要它变为 true,您就会收到通知,并且您可以通过传递特定的观察者来移除您的观察者。

【讨论】:

【参考方案11】:

Vince 和 Hakem Zaied 解决方案运行良好,但在我的情况下,我试图获取 livedata 实例并更新本地数据库,但 livedata 将首先从远程 API 更新,因此我得到了 NullPointer,所以我切换到observeForever,我能够在更新数据时获取数据,但现在我必须在获取数据后处理观察者,所以我修改了Vince解决方案,只在livedata包含数据时观察和发出数据。

   fun <T> LiveData<T>.observeOnce(observer: (T) -> Unit) 
    observeForever(object : Observer<T> 
        override fun onChanged(value: T) 

            //Resource is my data class response wrapper, with this i was able to 
            //only update the observer when the livedata had values
            //the idea is to cast the value to the expected type and check for nulls

            val resource = value as Resource<*>
            if (resource.data != null) 
                observer(value)
                removeObserver(this)
            
        )
    

【讨论】:

【参考方案12】:

这是一个 androidx.lifecycle.Observer Java 示例:

Observer <? super List<MyEntity>> observer = new Observer<List<MyEntity>>() 
    @Override
    public void onChanged(List<MyEntity> myEntities) 
        Log.d(TAG, "observer changed");

       MySearchViewModel.getMyList().removeObserver(this);
    
;
MySearchViewModel.getMyList().observe(MainActivity.this, observer);

【讨论】:

【参考方案13】:

在我看来,Livedata 旨在持续接收即将到来的数据。如果您只是希望它只执行一次,例如从服务器请求数据以初始化 UI,我建议您以这种方式设计代码:

1、将您的耗时方法定义为 Viewmodel 中的 non-Livedata 类型。您不必在此过程中启动新线程。

2、在一个Activity中启动一个new Thread,在新的Thread中,调用上面定义的方法,然后是runOnUiThread(),你可以在其中写下你使用请求数据的逻辑。这样耗时的方法不会阻塞 UI 线程,而是阻塞新线程,所以 runOnUiThread() 仅在您请求的数据成功接收后运行。

因此,如果这是您想要的,请考虑更换 Livedata。

【讨论】:

以上是关于LiveData 在第一次回调后删除观察者的主要内容,如果未能解决你的问题,请参考以下文章

为啥用 viewLifecycleOwner 观察到的 LiveData 在 onDestroyView 之后会得到回调?

LiveData详解

未调用 LiveData 观察者

Android Room LiveData观察器未更新

LiveData 观察()未调用

Android LiveData和MutableLiveData使用详解