仅在 firebase 调用完成时加载布局

Posted

技术标签:

【中文标题】仅在 firebase 调用完成时加载布局【英文标题】:Only load layout when firebase calls are complete 【发布时间】:2016-11-05 12:40:10 【问题描述】:

问题:我的 MainActivity 中的布局是在我有机会完成调用 firebase 以恢复应用数据之前生成的。如果我旋转屏幕,从而导致 onCreate 在 MainActivity 中再次运行,则一切都生成得很好。

在我的应用程序中,我有一个 Application 类的自定义实现,它会调用 Firebase 以恢复数据/确保数据始终同步。然而,我有大约 20 个 ValueEventListener,而不是只有几个 ValueEventListeners 下面有很多孩子。这是为了防止我的应用程序在每次用户生成少量数据时同步几乎整个数据库,并避免异步操作数据时可能发生的冲突。有趣的是,ValueEventListeners 实际上并没有按照它们编码的顺序来获取它们的数据,所以当最后一个完成时我不能将 bool 设置为 true。

我想知道是否有一种简单的方法可以检测 Firebase 读取是否全部完成,而不是在每个侦听器的末尾放置一些代码并手动执行此操作。我查看了 Application 类以及 Firebase 中可用的方法,但到目前为止我还没有找到任何可行的方法。

我的应用程序类中的一些代码:

public class CompassApp extends Application 

...然后在Application的onCreate里面:

//        Fetching Data from DB.
    FirebaseDatabase database = FirebaseDatabase.getInstance();
    DatabaseReference dbRef = database.getReference();

//        Current User Data
    dbRef.child("currentAppData").child("workingOut").addValueEventListener(new ValueEventListener() 
        @Override
        public void onDataChange(DataSnapshot dataSnapshot) 
            activeFirebaseConnections += 1;
//                Stops executing method if there is no data to retrieve
            if (!dataSnapshot.exists()) 
                return;
            

            workingOut = dataSnapshot.getValue(boolean.class);

            activeFirebaseConnections -= 1;

        

        @Override
        public void onCancelled(DatabaseError databaseError) 
            Log.e(TAG, "Firebase read of sleepDebt failed");
        
    );
    dbRef.child("currentAppData").child("sleeping").addValueEventListener(new ValueEventListener() 
        @Override
        public void onDataChange(DataSnapshot dataSnapshot) 
            activeFirebaseConnections += 1;
//                Stops executing method if there is no data to retrieve
            if (!dataSnapshot.exists()) 
                return;
            

            sleeping = dataSnapshot.getValue(boolean.class);

            activeFirebaseConnections -= 1;

        

        @Override
        public void onCancelled(DatabaseError databaseError) 
            Log.e(TAG, "Firebase read of sleepDebt failed");
        
    );

(等等……剩下的就是更多的ValueEventListeners)

【问题讨论】:

不描述你的代码,你能贴出[重现问题的最小代码]***.com/help/mcve)吗? @FrankvanPuffelen 从 Application 类中添加了一个 sn-p。实际扩展布局的代码使用了几个非常具体的库,因为它通过一些非常独特的应用程序逻辑,所以它可能比任何东西都更令人困惑。重要的是,根据调试器和日志,ValueEventListener 是在扩展布局的方法(在 MainActivity 的 onCreate 中)之后运行的。因此,当应用第一次加载时,布局总是会被默认数据填充。 我之前也尝试过在 Activity onCreate 本身中进行 firebase 调用的实现,虽然它在 100% 的时间里工作,但在 UI 生成中存在非常明显的延迟。 【参考方案1】:

我目前正在使用我为启动多个数据负载而创建的实用程序类。当所有侦听器都完成时,它将触发最终任务。他们在这里的关键是巧妙地使用新的Task 工具来进行 Play Services 提供的异步编程。您可能可以使用其他一些异步框架来完成所有这些工作,但任务是通过 Play 服务免费提供的,并且在 Firebase 的其他部分中使用,因此您不妨学习使用它们。 :-)

我在 2016 年的 Google I/O 上发表了关于任务的演讲。这是a link to a video,它直接跳转到该会话的相关部分。

public class FirebaseMultiQuery 

    private final HashSet<DatabaseReference> refs = new HashSet<>();
    private final HashMap<DatabaseReference, DataSnapshot> snaps = new HashMap<>();
    private final HashMap<DatabaseReference, ValueEventListener> listeners = new HashMap<>();

    public FirebaseMultiQuery(final DatabaseReference... refs) 
        for (final DatabaseReference ref : refs) 
            add(ref);
        
    

    public void add(final DatabaseReference ref) 
        refs.add(ref);
    

    public Task<Map<DatabaseReference, DataSnapshot>> start() 
        // Create a Task<DataSnapsot> to trigger in response to each database listener.
        //
        final ArrayList<Task<DataSnapshot>> tasks = new ArrayList<>(refs.size());
        for (final DatabaseReference ref : refs) 
            final TaskCompletionSource<DataSnapshot> source = new TaskCompletionSource<>();
            final ValueEventListener listener = new MyValueEventListener(ref, source);
            ref.addListenerForSingleValueEvent(listener);
            listeners.put(ref, listener);
            tasks.add(source.getTask());
        

        // Return a single Task that triggers when all queries are complete.  It contains
        // a map of all original DatabaseReferences originally given here to their resulting
        // DataSnapshot.
        //
        return Tasks.whenAll(tasks).continueWith(new Continuation<Void, Map<DatabaseReference, DataSnapshot>>() 
            @Override
            public Map<DatabaseReference, DataSnapshot> then(@NonNull Task<Void> task) throws Exception 
                task.getResult();
                return new HashMap<>(snaps);
            
        );
    

    public void stop() 
        for (final Map.Entry<DatabaseReference, ValueEventListener> entry : listeners.entrySet()) 
            entry.getKey().removeEventListener(entry.getValue());
        
        snaps.clear();
        listeners.clear();
    

    private class MyValueEventListener implements ValueEventListener 
        private final DatabaseReference ref;
        private final TaskCompletionSource<DataSnapshot> taskSource;

        public MyValueEventListener(DatabaseReference ref, TaskCompletionSource<DataSnapshot> taskSource) 
            this.ref = ref;
            this.taskSource = taskSource;
        

        @Override
        public void onDataChange(DataSnapshot dataSnapshot) 
            snaps.put(ref, dataSnapshot);
            taskSource.setResult(dataSnapshot);
        

        @Override
        public void onCancelled(DatabaseError databaseError) 
            taskSource.setException(databaseError.toException());
        
    


你使用它的方式是这样的。确定您需要从中加载数据并将它们存储在 Activity 成员中的所有 DatabaseReferences。然后,在您的活动 onStart() 期间,将它们全部传递到 FirebaseMultiQuery 的实例中并在其上调用 start()。它将返回一个生成 DatabaseReference 映射到它将生成的 DataSnapshot 的任务。使用该任务,注册一个最终的侦听器,该侦听器将在加载所有数据时触发:

firebaseMultiQuery = new FirebaseMultiQuery(dbRef1, dbRef2, dbRef3, ...);
final Task<Map<DatabaseReference, DataSnapshot>> allLoad = firebaseMultiQuery.start();
allLoad.addOnCompleteListener(activity, new AllOnCompleteListener());

在您的onStop() 中,不要忘记在其上调用stop() 以确保一切正常关闭:

firebaseMultiQuery.stop();

完成接收地图中所有数据的任务触发的侦听器可能如下所示:

private class AllOnCompleteListener implements OnCompleteListener<Map<DatabaseReference, DataSnapshot>> 
    @Override
    public void onComplete(@NonNull Task<Map<DatabaseReference, DataSnapshot>> task) 
        if (task.isSuccessful()) 
            final Map<DatabaseReference, DataSnapshot> result = task.getResult();
            // Look up DataSnapshot objects using the same DatabaseReferences you passed into FirebaseMultiQuery
        
        else 
            exception = task.getException();
            // log the error or whatever you need to do
        
        // Do stuff with views
        updateUi();
    

可能值得注意的是,写入数据的 Firebase 数据库调用也会返回您可以监听以完成的任务。此外,Firebase 存储使用任务,Firebase 身份验证也是如此。

此处的代码目前可能不适用于 Firebase 数据库查询,该查询不是对数据库中节点的简单命名引用。这取决于它们是否以与键入 HashMap 一致的方式进行散列。

上面的代码仍然是实验性的,但到目前为止我还没有遇到任何问题。看起来代码很多,但是如果你做很多并发加载,那真的很方便。

【讨论】:

谢谢 :) 这看起来很有希望,但也比我预期的要复杂得多。我将不得不花一些时间来解决这个问题...... 这对我来说似乎很棒.. 是否有任何关于它的博客文章详细解释了 Tasks API? developers.google.com/android/guides/tasks 还不够…… @kirtan403 我在 I/O '16 上详细谈到了它。你可以在这里观看(直接跳到关于任务的部分)。 youtu.be/AJqakuas_6g?t=6m28s也应该做个博客.... 我已经看了3遍以上。但有时书面解释的例子可以解决问题。一篇博文将对像我这样的 firebase 和异步任务的新手有所帮助 任何考虑使用这种方法的人都会受益于阅读 Doug 非常完整和详细的series of blog posts on Tasks。【参考方案2】:

我的应用也面临同样的问题。在对如何检查 firebase 是否已完成检索数据进行长期研究后,我找到了这个解决方案。

因此,当您的应用中有多个事件侦听器时。 Firebase 不会按顺序一一执行此侦听器。 值事件监听器总是最后执行。 因此,如果您想在检索到所有数据时执行某些操作,您可以调用 ValueEventListener 并执行一个将所有视图加载到其中的方法。

所以使用rootRef.addListenerForSingleValueEvent() 并在其onDataChanged() 中执行从Firebase 检索数据后要执行的操作。

这对我有用,希望它也对你有用

【讨论】:

问题是我有 20 个 ValueEventListener,所以它们只是以看似随机的顺序运行。我需要知道他们什么时候都跑完了。我想我可以在每一个中增加一个 int,并且我知道当我的 int = 我拥有的听众数量时,它就会完成。这感觉更像是一种解决方法而不是解决方案 您尝试过记录吗?检查ValueEventListeners 的执行模式?还尝试在最后一个ValueEventListener 中从 Firebase 检索数据后实现您想要执行的逻辑 它似乎尊重某种秩序,但背后似乎没有太多逻辑。我考虑过这一点,但是随着我的应用程序数据集的扩展,我很有可能会添加更多的侦听器,而且我不希望我的代码每次都中断......话虽如此,这可能是一个很好的解决方案。 【参考方案3】:

@Doug Stevenson 的回答很棒。对我来说几乎是开箱即用的。我唯一改变的不是显式创建 AllOnCompleteListener 类,而是编写了如下代码:

    private OnCompleteListener<Map<DatabaseReference, DataSnapshot>> allListener;...



allListener = new OnCompleteListener<Map<DatabaseReference, DataSnapshot>>() 
      @Override
      public void onComplete(@NonNull Task<Map<DatabaseReference, DataSnapshot>> task) 
          if (task.isSuccessful()) 
          final Map<DatabaseReference, DataSnapshot> result = task.getResult();
          // Look up DataSnapshot objects using the same DatabaseReferences you passed into FirebaseMultiQuery



          for (Map.Entry<DatabaseReference, DataSnapshot> entry : result.entrySet())  ...

【讨论】:

以上是关于仅在 firebase 调用完成时加载布局的主要内容,如果未能解决你的问题,请参考以下文章

AngularJS仅在父控制器完成异步调用时加载控制器

如果超时或无法访问服务器,Fire base 不会调用 onCancelled

只有在 SwiftUI 和 Firebase 中完成循环中的异步调用时,如何才能返回函数?

node_modules/@angular/fire/firebase.app.module.d.ts 中的错误?

Firebase .childAdded 和活动指示器

如何退出firebase服务 - 仅在终端中运行