Установка значения свойства Singleton в Firebase Listener

В настоящее время я тестирую Firebase вместе с моделью Singleton, которую планирую использовать для доступа в течение всего жизненного цикла всего приложения. Теперь я застрял с чем-то, что кажется действительно тривиальным, но я не могу понять это для моей жизни. У меня есть образец модели, которую я использую: Закладки в 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);
}

Теперь, когда я запускаю mainActivity с экземпляром набора Singleton, я получаю нулевую ошибку, потому что ясно, что функция, которую я написал для загрузки данных модели из 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

Как я могу сделать эту работу?

3 ответа

Решение

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 firebaseError) { }
    });
    Log.v("Async101", "Returning loaded bookmarks");
    setBookmarks(loadedBookmarks);
}

Вопреки тому, что вы, вероятно, ожидаете, порядок операторов журнала будет:

Start loading bookmarks
Returning loaded bookmarks
Done loading bookmarks

У вас есть два варианта решения асинхронного характера этой загрузки:

  1. раздавить асинхронную ошибку (обычно сопровождаемую бормотанием фраз вроде: "это была ошибка, эти люди не знают, что делают")

  2. обнимите асинхронного зверя (обычно сопровождаемого проклятиями несколько часов, но через некоторое время мирными и лучше управляемыми приложениями)

Возьми синюю таблетку - сделай асинхронный вызов синхронным

Если вы хотите выбрать первый вариант, хорошо выполненный примитив синхронизации поможет вам:

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 firebaseError) { throw firebaseError.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 firebaseError) { throw firebaseError.toException(); }
    });
}

В приведенном выше коде мы не просто ждем события с одним значением, мы имеем дело со всеми из них. Это означает, что всякий раз, когда закладки меняются, onDataChange выполняется, и мы (повторно) загружаем пример данных (или любое другое действие, соответствующее потребностям вашего приложения).

Обновление (20171227): чтобы сделать код более пригодным для повторного использования, вам может потребоваться определить собственный интерфейс обратного вызова, а не вызывать точный код в onDataChange, Посмотрите на этот ответ для хорошего примера этого.

TL;DR: принять асинхронность Firebase

Как я упоминал в другом посте, вы можете справиться с асинхронной природой 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 для Android предоставляет каркас задач (как Parse сделал с Bolts), который похож на концепцию обещаний JavaScript.

Сначала вы создаете 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();
    }

}

Теперь, когда у вас есть идея, предложите setBookmarks а также loadSampleData также асинхронны. Вы также можете создать их как 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) {
        // ...
    }
}

Вы должны инициализировать ваш Singleton, когда класс загружен. Поместите это в свой код:

private static BookSingleton  model = new BookSingleton();

private BookSingleton() {
}

public static BookSingleton getModel() {     return model == null ? new BookSingleton() : model;
}
Другие вопросы по тегам