java - Establecer el valor de la propiedad Singleton en Firebase Listener
android firebase-database (3)
TL; DR: Abraza la asincronía de Firebase
Como mencioné en otra post , puedes lidiar con la naturaleza asincrónica de Firebase usando promesas. Sería así:
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 para Android proporciona un marco de tareas (como lo hizo Parse con Bolts ), que es similar al concepto de promesas de JavaScript .
Primero crea una
Task
para descargar el marcador de 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();
}
}
Ahora que tiene la idea, suponga que
setBookmarks
y
loadSampleData
también son asíncronos.
También puede crearlos como tareas de
Continuation
(al igual que la anterior) que se ejecutarán en secuencia:
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) {
// ...
}
}
Actualmente estoy probando Firebase junto con un modelo Singleton que planeo usar para acceder durante el ciclo de vida de toda la aplicación. Ahora estoy atrapado con algo que parece realmente trivial, pero no puedo entenderlo por mi vida. Tengo una muestra del modelo que uso: Marcadores en 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);
}
Ahora, cuando inicio mainActivity con la instancia del conjunto Singleton, obtengo un error nulo porque claramente la función que escribí para cargar los datos del modelo desde firebase no establece nada.
Algo como esto:
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
¿Cómo puedo hacer que esto funcione?
Debe inicializar su Singleton cuando se carga la clase. Pon esto en tu código:
private static BookSingleton model = new BookSingleton();
private BookSingleton() {
}
public static BookSingleton getModel() { return model == null ? new BookSingleton() : model;
}
Firebase carga y sincroniza datos de
forma asincrónica
.
Por lo tanto, su
loadModelWithDataFromFirebase()
no espera a que finalice la carga, solo
comienza a
cargar los datos de la base de datos.
Para cuando
loadModelWithDataFromFirebase()
función
loadModelWithDataFromFirebase()
, la carga aún no ha finalizado.
Puede probar esto fácilmente con algunas declaraciones de registro bien ubicadas:
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);
}
Al contrario de lo que probablemente espera, el orden de las declaraciones de registro será:
Start loading bookmarks
Returning loaded bookmarks
Done loading bookmarks
Tiene dos opciones para lidiar con la naturaleza asincrónica de esta carga:
-
aplastar el error asincrónico (generalmente acompañado de murmullos de frases como: "fue un error, estas personas no saben lo que están haciendo")
-
abraza a la bestia asincrónica (generalmente acompañada de bastantes horas de maldiciones, pero después de un tiempo de paz y mejores aplicaciones)
Tome la píldora azul: haga que la llamada asincrónica se comporte de forma sincrónica
Si tiene ganas de elegir la primera opción, una primitiva de sincronización bien colocada hará el truco:
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);
}
Actualización (20160303) : cuando acabo de probar esto en Android, bloqueó mi aplicación. Funciona en un JVM normal bien, pero Android es más meticuloso cuando se trata de subprocesos. Siéntase libre de intentarlo y hacerlo funcionar ... o
Tome la píldora roja: aborde la naturaleza asincrónica de la sincronización de datos en Firebase
Si en su lugar elige adoptar la programación asincrónica, debe repensar la lógica de su aplicación.
Actualmente tiene "Primero cargue los marcadores. Luego cargue los datos de muestra. Y luego cargue aún más".
Con un modelo de carga asíncrono, debería pensar como "Cada vez que se cargan los marcadores, quiero cargar los datos de muestra. Cada vez que se cargan los datos de muestra, quiero cargar aún más".
La ventaja de pensar de esta manera es que también funciona cuando los datos pueden cambiar constantemente y, por lo tanto, se sincronizan varias veces: "Cada vez que cambian los marcadores, también quiero cargar los datos de la muestra. Cada vez que los datos de la muestra cambian, quiero cargar incluso más."
En el código, esto conduce a llamadas anidadas o cadenas de eventos:
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(); }
});
}
En el código anterior, no solo esperamos un evento de valor único, sino que nos ocupamos de todos ellos.
Esto significa que cada vez que se cambian los marcadores, se ejecuta
onDataChange
y (re)
onDataChange
los datos de muestra (o cualquier otra acción que se ajuste a las necesidades de su aplicación).
Para que el código sea más reutilizable, es posible que desee definir su propia interfaz de devolución de llamada, en lugar de llamar al código preciso en
onDataChange
.
Echa un vistazo a
esta respuesta
para un buen ejemplo de eso.