【问题标题】:How to remove OnCompleteListener on .get() method when fragment is destroyed?片段被销毁时如何在 .get() 方法上删除 OnCompleteListener?
【发布时间】:2021-01-16 22:27:33
【问题描述】:

我的项目使用 Firestore 作为数据库。我有一个片段,它在打开时会在其 onCreate 方法中从 Firestore 请求一个文档。我已将 OnCompleteListener 附加到 .get() 方法,以便在文档获取完成时更新 UI。

问题在于,有时用户在打开片段后会在触发 onCompleteListener 之前快速移动到另一个片段。或者在某些情况下,同一个 Fragment 被调用两次,当 Fragment 的第一个实例被销毁时,它的 onCompleteListener 仍然存在,并在 Fragment 的第一个实例被销毁后触发。在这两种情况下,我都会遇到异常

Fatal Exception: java.lang.IllegalStateException: Fragment ProfileFragment{4ee762 (a8f7ae01-23be-4d47-b695-68e273b992bf)} not attached to a context.
       at androidx.fragment.app.Fragment.requireContext(Fragment.java:774)
       at androidx.fragment.app.Fragment.getResources(Fragment.java:838)
       at androidx.fragment.app.Fragment.getString(Fragment.java:860)
       at com.desivideshi.productinfo.ProfileFragment$18.onComplete(ProfileFragment.java:903)
       at com.google.android.gms.tasks.zzj.run(com.google.android.gms:play-services-tasks@@17.1.0:4)
       at android.os.Handler.handleCallback(Handler.java:873)
       at android.os.Handler.dispatchMessage(Handler.java:99)
       at com.google.android.gms.internal.tasks.zzb.dispatchMessage(com.google.android.gms:play-services-tasks@@17.1.0:6)
       at android.os.Looper.loop(Looper.java:193)
       at android.app.ActivityThread.main(ActivityThread.java:6898)
       at java.lang.reflect.Method.invoke(Method.java)
       at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:537)
       at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)

这是我在 ProfileFragment 的 onCreate 方法中的代码 sn-p

documentReference.get().addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
                        @Override
                        public void onComplete(@NonNull Task<DocumentSnapshot> task) {
                            if (mContext != null){
                                if (task.isSuccessful() && task.getResult() != null && task.getResult().getData() != null) {
                                        Map<String, Object> productsDoc = task.getResult().getData();
                                        productsList = (List<Map<String, String>>) productsDoc.get(getString(R.string.fs_f_products_list));
                                   
                                    if(productsList == null){
                                        productsList = new ArrayList<>();
                                    }
                                      mMyProfileData.setMyProductsList(productsList);
                                    }
                                } else {
                                    Toast.makeText(mContext, "Failed to load products", Toast.LENGTH_SHORT).show();
                                }
                                createTabLayout(); 
                                profileTabLayout.selectTab(profileTabLayout.getTabAt(profileTabLayout.getSelectedTabPosition()));
                                progressLayoutProfileTab.setVisibility(View.GONE);
                            }
                        }
                    });

有没有办法删除对 OnCompleteListener 的回调,我可以将它放在 Fragment 的 onDestry()onDetach() 方法中以避免此异常?

【问题讨论】:

    标签: java android firebase android-fragments google-cloud-firestore


    【解决方案1】:

    fragment 销毁时如何在 .get() 方法上移除 OnCompleteListener?

    不需要删除任何监听器,因为get() 方法只获取一次数据,然后就结束了。如果你使用过addSnapshotListener(),你应该已经移除了监听器according to the life-cycle of your activity。在您的情况下,onComplete() 方法仅在数据完全从数据库加载时才会触发。您需要知道的是,Cloud Firestore 客户端在后台线程中运行所有网络操作。所以不要在后台线程中做与上下文相关的事情。

    如果您需要实时监听器,请使用 addSnapshotListener() 并在您的活动/片段停止后停止监听更改。另请记住,onDestroy 并不总是被调用,因此请停止监听 onStop() 的变化。

    编辑:

    因为你在回调中使用了与上下文相关的东西,所以你应该只查询数据库是isAdded()方法返回true

    【讨论】:

    • 我不打算使用快照监听器,因为我只需要一次数据。即使 get() 方法只获取一次数据,获取数据也会有几百毫秒到几秒的延迟。如果片段在这么短的时间内被破坏,我需要处理此案。
    • 在这种情况下,请确保 Fragment 确实附加到活动。为了解决这个问题,你应该在你的片段中调用isAdded() 方法,并且只有当这个方法返回 true 时才执行你的逻辑。
    • 因为你在回调中使用了与上下文相关的东西,所以你应该只查询数据库是isAdded()return true。
    • 如果您可以编辑您的答案以包含isAdded() 建议,我会接受答案。
    【解决方案2】:

    如果您要直接在侦听器中更改视图(这不是一个好主意,稍后会详细介绍),那么您应该在 get() 返回的 Task 对象上使用 activity-scoped listener , 这样监听器就不会在活动停止后触发。

    documentReference.get().addOnCompleteListener(activity, callback)
    

    请注意,您将活动实例作为第一个参数传递,在回调之前。

    但是,现代 Android 应用程序最好使用 MVVM architecture,正如 Google 在整个 Android 文档中所建议的那样,通过仅公开一个 LiveData 来从视图中抽象出数据库代码,该 LiveData 知道活动生命周期和活动停止后不会向观察者发出事件。但这完全取决于您。

    【讨论】:

    • 我知道将活动作为第一个参数传递可以解决活动变化的问题。但在这里我只改变片段而不是活动。我没有找到任何类似的替代方法来传递片段。作为一种解决方法,我目前正在使用 Alex 的建议在 onCompleteListener 中使用isAdded() 检查。我一定会看看你推荐的推荐的 MVVM 架构。
    【解决方案3】:

    我认为最简单的方法是使用try catch,因此当您收到异常时,什么都不会发生,这是一个受控异常。

    解决此问题的最佳方法是将 MVVM 模式与 LiveData 结合使用。 ViewModel 知道 Activity / Fragment 的生命周期,当您的 Fragment 死亡时,您不会从 Fragment 处于活动状态时所做的调用获得任何后台通知。

    试试这个

    documentReference.get().addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
                            @Override
                            public void onComplete(@NonNull Task<DocumentSnapshot> task) {
                                try{
                                if (mContext != null){
                                    if (task.isSuccessful() && task.getResult() != null && task.getResult().getData() != null) {
                                            Map<String, Object> productsDoc = task.getResult().getData();
                                            productsList = (List<Map<String, String>>) productsDoc.get(getString(R.string.fs_f_products_list));
                                       
                                        if(productsList == null){
                                            productsList = new ArrayList<>();
                                        }
                                          mMyProfileData.setMyProductsList(productsList);
                                        }
                                    } else {
                                        Toast.makeText(mContext, "Failed to load products", Toast.LENGTH_SHORT).show();
                                    }
                                    createTabLayout(); 
                                    profileTabLayout.selectTab(profileTabLayout.getTabAt(profileTabLayout.getSelectedTabPosition()));
                                    progressLayoutProfileTab.setVisibility(View.GONE);
                                }
                              }catch(Exception e){
                                //Use the exception inside a log, let it blank or as you prefer
                              }
                            }
                        });
    

    【讨论】:

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