Je teste actuellement Firebase avec un modèle Singleton que je prévois d'utiliser pour accéder pendant le cycle de vie de l'application entière. Je suis maintenant coincé avec quelque chose qui semble vraiment insignifiant mais je ne peux pas le comprendre pour la vie de moi. J'ai un échantillon du modèle que j'utilise: Signets dans 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);
}
Maintenant, lorsque je démarre la mainActivity avec l'instance de l'ensemble Singleton, j'obtiens une erreur nulle, car la fonction que j'ai écrite pour charger les données du modèle à partir de Firebase ne définit clairement rien.
Quelque chose comme ceci: 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
Comment puis-je faire fonctionner cela?
Firebase charge et synchronise les données de manière asynchrone . Ainsi, votre loadModelWithDataFromFirebase()
n'attend pas la fin du chargement, elle commence juste le chargement des données depuis la base de données. Au moment où votre fonction loadModelWithDataFromFirebase()
revient, le chargement n'est pas encore terminé.
Vous pouvez facilement tester cela par vous-même avec des instructions de journal bien placées:
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);
}
Contrairement à ce que vous attendez probablement, l'ordre des instructions de journal sera:
Start loading bookmarks
Returning loaded bookmarks
Done loading bookmarks
Vous avez deux choix pour gérer la nature asynchrone de ce chargement:
écraser le bug asynchrone (généralement accompagné de murmures de phrases comme: "c'était une erreur, ces gens ne savent pas ce qu'ils font")
embrasser la bête asynchrone (généralement accompagnée de quelques heures de malédiction, mais après un certain temps par la paix et des applications mieux comportées)
Si vous avez envie de choisir la première option, une primitive de synchronisation bien placée fera l'affaire:
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);
}
Mise à jour (20160303) : lorsque je viens de tester cela sur Android, cela a bloqué mon application. Cela fonctionne sur une amende JVM régulière, mais Android est plus capricieux en ce qui concerne le filetage. N'hésitez pas à essayer de le faire fonctionner ... ou
Si vous choisissez plutôt d'adopter la programmation asynchrone, vous devez repenser la logique de votre application.
Vous avez actuellement "Chargez d'abord les signets. Chargez ensuite les données d'exemple. Et chargez-en encore plus."
Avec un modèle de chargement asynchrone, vous devriez penser comme "Chaque fois que les signets se sont chargés, je veux charger les données d'exemple. Chaque fois que les données d'échantillon ont été chargées, je veux charger encore plus."
L'avantage de penser de cette façon est que cela fonctionne également lorsque les données peuvent être constamment changées et donc synchronisées plusieurs fois: "Chaque fois que les signets changent, je veux également charger les données d'exemple. Chaque fois que les données d'échantillon changent, je veux charger même plus."
En code, cela conduit à des appels imbriqués ou à des chaînes d'événements:
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(); }
});
}
Dans le code ci-dessus, nous n'attendons pas seulement un événement de valeur unique, nous les traitons plutôt tous. Cela signifie que chaque fois que les signets sont modifiés, le onDataChange
est exécuté et nous (re) chargeons les exemples de données (ou toute autre action correspondant aux besoins de votre application).
Pour rendre le code plus réutilisable, vous souhaiterez peut-être définir votre propre interface de rappel, au lieu d'appeler le code précis dans onDataChange
. Jetez un oeil à cette réponse pour un bon exemple de cela.
Comme je l'ai mentionné dans un autre post , vous pouvez gérer la nature asynchrone de Firebase en utilisant des promesses. Ce serait comme ça:
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);
}
Google API pour Android fournit un cadre de tâches (tout comme Parse l'a fait avec Bolts ), qui est similaire à - JavaScript promet concept.
Vous créez d'abord un Task
pour télécharger le signet depuis 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();
}
}
Maintenant que vous avez l'idée, supposez que setBookmarks
et loadSampleData
sont également asynchrones. Vous pouvez également les créer sous forme de tâches Continuation
(comme la précédente) qui s'exécuteront en séquence:
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) {
// ...
}
}
Vous devez initialiser votre Singleton lorsque la classe est chargée. Mettez ceci sur votre code:
private static BookSingleton model = new BookSingleton();
private BookSingleton() {
}
public static BookSingleton getModel() { return model == null ? new BookSingleton() : model;
}