Ustawianie wartości właściwości Singleton w Firebase Listener

Obecnie testuję bazę Firebase wraz z Modelem Singleton, który planuję wykorzystać, aby uzyskać dostęp podczas cyklu życia całej aplikacji. Utknąłem z czymś, co wydaje się banalne, ale nie mogę tego rozgryźć do końca życia. Mam próbkę modelu, którego używam: zakładki w 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);
}

Teraz, kiedy uruchamiam mainActivity z instancją zestawu Singleton dostaję błąd null, ponieważ wyraźnie funkcja, którą napisałem, aby załadować dane modelu z zestawów firebase nic.

Coś takiego: 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
Jak mogę to zrobić?
Author: CopsOnRoad, 2015-10-19

3 answers

Firebase ładuje i synchronizuje dane asynchronicznie. Więc twoja loadModelWithDataFromFirebase() nie czeka na zakończenie ładowania, po prostu rozpoczyna ładowanie danych z bazy danych. Do czasu powrotu funkcji loadModelWithDataFromFirebase() ładowanie nie zostało jeszcze zakończone.

Możesz łatwo przetestować to za pomocą dobrze umieszczonych instrukcji dziennika:

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);
}

Wbrew oczekiwaniom, kolejność wyrażeń dziennika będzie następująca:

Start loading bookmarks
Returning loaded bookmarks
Done loading bookmarks

Masz dwa wyjścia do rozdania z asynchronicznym charakterem tego obciążenia:

  1. Zmiażdżyć błąd asynchroniczny (zwykle towarzyszy mu mruczenie zwrotów typu: "to był błąd, ci ludzie nie wiedzą co robią")

  2. Embrace the asynchronous beast (zwykle towarzyszy mu kilka godzin przeklinania, ale po pewnym czasie przez spokój i lepiej zachowujące się aplikacje)

Weź niebieską pigułkę - spraw, by asynchroniczne wywołanie zachowywało się synchronicznie

Jeśli czujesz podobnie jak wybranie pierwszej opcji, dobrze umiejscowiona synchronizacja primitive zrobi sztuczkę:

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);
}

Update (20160303) : kiedy właśnie testowałem to na Androidzie, zablokował moją aplikację. Działa na zwykłym JVM dobrze, ale Android jest bardziej wybredny, jeśli chodzi o gwintowanie. Nie krępuj się, aby to zadziałało... lub

[[30]}Weź czerwoną pigułkę - poradź sobie z asynchronicznym charakterem synchronizacji danych w bazie Firebase [31]}

Jeśli zamiast tego zdecydujesz się na programowanie asynchroniczne, powinieneś przemyśleć logikę aplikacji.

Aktualnie masz "najpierw załaduj zakładki. Następnie załaduj dane próbki. A potem załaduj jeszcze więcej."

Z asynchronicznym modelem ładowania, powinieneś myśleć w stylu " gdy zakładki zostały załadowane, chcę załadować przykładowe dane. Za każdym razem, gdy dane próbki zostały załadowane, chcę załadować jeszcze więcej."

Zaletą takiego myślenia jest to, że działa również wtedy, gdy dane mogą się stale zmieniać, a tym samym zsynchronizować wiele times: "za każdym razem, gdy zakładki się zmieniają, chcę również załadować przykładowe dane. Za każdym razem, gdy dane próbki ulegną zmianie, chcę załadować jeszcze więcej."

W kodzie prowadzi to do zagnieżdżonych wywołań lub łańcuchów zdarzeń:

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(); }
    });
}

W powyższym kodzie nie tylko czekamy na pojedyncze zdarzenie wartości, ale zajmujemy się nimi wszystkimi. Oznacza to, że za każdym razem, gdy zakładki są zmieniane, wykonywana jest onDataChange i (ponownie)wczytujemy przykładowe dane (lub inną akcję pasującą do Twojej aplikacji potrzeb).

Update (20171227): aby Kod był bardziej wielokrotnego użytku, możesz zdefiniować własny interfejs wywołania zwrotnego, zamiast wywoływać dokładny kod w onDataChange. Zobacz ten anwer, aby znaleźć na to dobry przykład.

 20
Author: Frank van Puffelen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-12-27 16:01:33

TL; DR: Embrace Firebase Asynchroniczność

Jak wspomniałem w innym poście, możesz poradzić sobie z asynchronicznym charakterem Firebase za pomocą obietnic. Byłoby tak:

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.zadania

Google API dla Androida zapewnia Framework zadań (podobnie jak Parse zrobił z śrubami), który jest podobny do JavaScript promises.

Najpierw tworzysz Task na pobieranie zakładki z 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();
    }

}

Teraz, Kiedy masz pomysł, pomyśl, że setBookmarks i loadSampleData są również asynchroniczne. Możesz również utworzyć je jako Continuation zadania (podobnie jak poprzednie), które będą działać w kolejności:

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) {
        // ...
    }
}
 3
Author: JP Ventura,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:03:06

Musisz zainicjować swój Singleton, gdy klasa jest załadowana. Umieść to na swoim kodzie:

private static BookSingleton  model = new BookSingleton();

private BookSingleton() {
}

public static BookSingleton getModel() {     return model == null ? new BookSingleton() : model;
}
 0
Author: Gilson Silva,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-10-18 21:32:03