Установка значения свойства 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
У вас есть два варианта решения асинхронного характера этой загрузки:
раздавить асинхронную ошибку (обычно сопровождаемую бормотанием фраз вроде: "это была ошибка, эти люди не знают, что делают")
обнимите асинхронного зверя (обычно сопровождаемого проклятиями несколько часов, но через некоторое время мирными и лучше управляемыми приложениями)
Возьми синюю таблетку - сделай асинхронный вызов синхронным
Если вы хотите выбрать первый вариант, хорошо выполненный примитив синхронизации поможет вам:
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;
}