在 Firebase 侦听器中设置 Singleton 属性值

Posted

技术标签:

【中文标题】在 Firebase 侦听器中设置 Singleton 属性值【英文标题】:Setting Singleton property value in Firebase Listener 【发布时间】:2016-01-17 03:34:01 【问题描述】:

我目前正在测试 Firebase 以及我计划在整个应用的生命周期中使用的单例模型。我现在陷入了一些看似微不足道的事情,但我一生都无法弄清楚。我有一个我使用的模型示例:Firebase 中的书签。

public class BookSingleton 



private static BookSingleton model;

private ArrayList<BookMark> bookmarks = new ArrayList<BookMark>();


public static BookSingleton getModel()

    if (model == null)
    
        throw new IllegalStateException("The model has not been initialised yet.");
    

    return model;



public ArrayList<Bookmark> theBookmarkList()

    return this.bookmarks;



public void setBookmarks(ArrayList<Bookmark> bookmarks)
    this.bookmarks = bookmarks;



public void loadModelWithDataFromFirebase()
    Firebase db = new Firebase(//url);
    Firebase bookmarksRef = fb.child(//access correct child);


    final ArrayList<Bookmark> loadedBookmarks = new ArrayList<Bookmark>();
    bookmarksRef.addListenerForSingleValueEvent(new ValueEventListener() 
        @Override
        public void onDataChange(DataSnapshot dataSnapshot) 
                    //getting all properties from firebase...
                    Bookmark bookmark = new Bookmark(//properties here);
                    loadedBookmarks.add(bookmark);



                
            
            //bookmarks still exist here at this point
            setBookmarks(loadedBookmarks);

        

        @Override
        public void onCancelled(FirebaseError firebaseError) 

        
    );
    //by now loadedBookmarks is empty
    //this is probably the issue?
    //even without this line bookmarks is still not set in mainactivity
    setBookmarks(loadedBookmarks);

现在,当我使用 Singleton 集的实例启动 mainActivity 时,我得到一个 null 错误,因为显然我编写的从 firebase 加载模型数据的函数没有设置任何内容。

类似这样的: MainActivity

public class MainActivity extends AppCompatActivity 

private BookSingleton theModel;



@Override
protected void onCreate(Bundle savedInstanceState) 

    super.onCreate(savedInstanceState);

    // Load the model
    theModel = BookSingleton.getModel(this);
      //manually setting this works 
      //        ArrayList<Book> bookSamples = new ArrayList<Book>;
      //        bookSamples.add(aBookSample);

    theModel.loadModelWithSampleData(bookSamples);
    //should have set the singleton model property Bookmarks to the results from firebase

    theModel.loadModelWithDataFromFirebase();
    //returns 0
    Log.d(TAG, "" + theModel.theBookmarkList().size());


    setContentView(R.layout.activity_main);

    //......rest of code

我怎样才能做到这一点?

【问题讨论】:

不要使用单例。完全避免它。 这真的很快,是的,我可能会跳过使用 Singleton,但我仍然想知道为什么,因为即使不使用 Singleton,我也可能会再次遇到问题。 【参考方案1】:

Firebase 异步加载和同步数据。所以您的loadModelWithDataFromFirebase() 不会等待加载完成,它只是开始 从数据库加载数据。当你的loadModelWithDataFromFirebase() 函数返回时,加载还没有完成。

您可以使用一些放置良好的日志语句轻松地自行测试:

public void loadModelWithDataFromFirebase()
    Firebase db = new Firebase(//url);
    Firebase bookmarksRef = fb.child(//access correct child);

    Log.v("Async101", "Start loading bookmarks");
    final ArrayList<Bookmark> loadedBookmarks = new ArrayList<Bookmark>();
    bookmarksRef.addListenerForSingleValueEvent(new ValueEventListener() 
        @Override
        public void onDataChange(DataSnapshot dataSnapshot) 
            Log.v("Async101", "Done loading bookmarks");
            //getting all properties from firebase...
            Bookmark bookmark = new Bookmark(//properties here);
            loadedBookmarks.add(bookmark);
        

        @Override
        public void onCancelled(FirebaseError error)  throw error.toException(); 
    );
    Log.v("Async101", "Returning loaded bookmarks");
    setBookmarks(loadedBookmarks);

与您可能期望的相反,日志语句的顺序将是:

Start loading bookmarks
Returning loaded bookmarks
Done loading bookmarks

您有两种选择来处理这种加载的异步性质:

    消除异步错误(通常伴随着诸如“这是一个错误,这些人不知道他们在做什么”之类的喃喃自语)

    拥抱异步野兽(通常伴随着好几个小时的诅咒,但过一段时间是和平和表现更好的应用程序)

采取蓝色药丸 - 使异步调用同步运行

如果您想选择第一个选项,一个放置得当的同步原语就可以了:

public void loadModelWithDataFromFirebase() throws InterruptedException 
    Firebase db = new Firebase(//url);
    Firebase bookmarksRef = fb.child(//access correct child);

    Semaphore semaphore = new Semaphore(0);

    final ArrayList<Bookmark> loadedBookmarks = new ArrayList<Bookmark>();
    bookmarksRef.addListenerForSingleValueEvent(new ValueEventListener() 
        @Override
        public void onDataChange(DataSnapshot dataSnapshot) 
            Bookmark bookmark = new Bookmark(//properties here);
            loadedBookmarks.add(bookmark);
            semaphore.release();
        

        @Override
        public void onCancelled(FirebaseError error)  throw error.toException(); 
    );
    semaphore.acquire();
    setBookmarks(loadedBookmarks);

更新 (20160303):当我刚刚在 android 上测试时,它阻止了我的应用。它可以在常规 JVM 上正常工作,但 Android 在线程方面更加挑剔。随意尝试让它工作......或

采取红色药丸 - 处理 Firebase 中数据同步的异步性质

如果您选择采用异步编程,则应重新考虑应用程序的逻辑。

您目前有“首先加载书签。然后加载示例数据。然后加载更多。”

对于异步加载模型,您应该这样想“只要加载了书签,我就想加载示例数据。只要加载了示例数据,我就想加载更多。”

这样思考的好处是,当数据可能不断变化并因此多次同步时,它也可以工作:“每当书签发生变化时,我也想加载示例数据。每当示例数据发生变化时,我想要加载更多。”

在代码中,这会导致嵌套调用或事件链:

public void synchronizeBookmarks()
    Firebase db = new Firebase(//url);
    Firebase bookmarksRef = fb.child(//access correct child);

    final ArrayList<Bookmark> loadedBookmarks = new ArrayList<Bookmark>();
    bookmarksRef.addValueEventListener(new ValueEventListener() 
        @Override
        public void onDataChange(DataSnapshot dataSnapshot) 
            Bookmark bookmark = new Bookmark(//properties here);
            loadedBookmarks.add(bookmark);
            setBookmarks(loadedBookmarks);
            loadSampleData();
        

        @Override
        public void onCancelled(FirebaseError error)  throw error.toException(); 
    );

在上面的代码中,我们不只是等待单个值事件,而是处理所有事件。这意味着无论何时更改书签,都会执行 onDataChange 并且我们(重新)加载示例数据(或任何其他适合您的应用程序需要的操作)。

为了使代码更可重用,您可能需要定义自己的回调接口,而不是调用onDataChange 中的精确代码。看看this answer 就是一个很好的例子。

【讨论】:

Semaphores 对我来说看起来真的很老套,所以我没有尝试实现它。但是,您的解释确实帮助我理解了 Firebase 的异步特性,这让我考虑改为在事件侦听器中设置所有逻辑。非常感谢:D 那么这是否意味着不应该在 firebase 数据库中使用层的概念。例如,我想编写一个 DataAccess 层,在其中我将分别调用每个查询,然后在我的 Activity 的 DataAccess 类中调用此方法,但我开始怀疑这是否正确。所以我应该把我的查询代码粘贴到每个活动中还是什么? 您可以使用 CountDownLatch 只需几行即可轻松转换为同步。有时事情不能很好地结合在一起,这变得很有必要。例如,小部件不支持异步。 @AnthonyWijaya 哦不,绝不是 hacky!这是信号量(以及互斥量)的主要用途之一!也就是说,就像弗兰克先生所说的那样,可能会导致 Android 应用程序出现阻塞/ANR/崩溃……所以这将是接受异步调用的正当理由。【参考方案2】:

TL;DR:拥抱 Firebase 异步

正如我在另一个 post 中提到的,您可以使用 Promise 处理 Firebase 的异步特性。应该是这样的:

public Task<List<Data>> synchronizeBookmarks(List<Bookmark> bookmarks) 
     return Tasks.<Void>forResult(null)
        .then(new GetBook())
        .then(new AppendBookmark(bookmarks))
        .then(new LoadData())


public void synchronizeBookmarkWithListener() 
     synchronizeBookmarks()
         .addOnSuccessListener(this)
         .addOnFailureListener(this);

com.google.android.gms.tasks

Google API for Android 提供了一个task framework(就像Parse 对Bolts 所做的那样),类似于javascript promises 的概念。

首先,您创建一个Task 用于从 Firebase 下载书签:

class GetBook implements Continuation<Void, Task<Bookmark>> 

    @Override
    public Task<Bookmark> then(Task<Void> task) 
        TaskCompletionSource<Bookmark> tcs = new TaskCompletionSource();

        Firebase db = new Firebase("url");
        Firebase bookmarksRef = db.child("//access correct child");

        bookmarksRef.addValueEventListener(new ValueEventListener() 
            @Override
            public void onDataChange(DataSnapshot dataSnapshot) 
                tcs.setResult(dataSnapshot.getValue(Bookmark.class));
            
        );

        tcs.getTask();
    


现在你已经明白了,假设setBookmarksloadSampleData 也是异步的。您也可以将它们创建为Continuation 任务(就像上一个),它们将按顺序运行:

class AppendBookmark(List<Bookmark> bookmarks) implements
    Continuation<List<Bookmark>, Task<Bookmark> 

    final List<Bookmark> bookmarks;

    LoadBookmarks(List<Bookmark> bookmarks) 
        this.bookmark = bookmark;
    

    @Override
    Task<List<Bookmark>> then(Task<Bookmark> task) 
        TaskCompletionSource<List<Bookmark>> tcs = new TaskCompletionSource();
        bookmarks.add(task.getResult());         
        tcs.setResult(this.bookmarks);
        return tcs.getTask();
    


class LoadSampleData implements Continuation<List<Bookmark>, List<Data>> 
    @Override
    public Task<List<Data>> then(Task<List<Bookmark>> task) 
        // ...
    

【讨论】:

为努力使用TaskCompletionSource API 点赞...它是一个漂亮的基于任务的实用程序...(尽管它是一个经过美化的重新打包信号量...:P)【参考方案3】:

你必须在加载类时初始化你的单例。 把它放在你的代码上:

private static BookSingleton  model = new BookSingleton();

private BookSingleton() 


public static BookSingleton getModel()      return model == null ? new BookSingleton() : model;

【讨论】:

以上是关于在 Firebase 侦听器中设置 Singleton 属性值的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Flutter 中设置不同的 firebase 环境

在 Vue.js + Firebase 中设置项目的更好方法是啥? [复制]

如何在 Flutter Web 的 Firebase 分析中设置用户属性?

在 xcode 中设置 Firebase 的问题

如何在 Flutter Web 项目中设置 Firebase 功能

无法在 android studio 中设置 firebase