Establecer el valor de la propiedad Singleton en Firebase Listener

Resuelto Anthony Wijaya asked hace 54 años • 3 respuestas

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 en algo que parece realmente trivial pero que no puedo resolver 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, aparece 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?

Anthony Wijaya avatar Jan 01 '70 08:01 Anthony Wijaya
Aceptado

Firebase carga y sincroniza datos de forma asincrónica . Entonces loadModelWithDataFromFirebase()no espera a que finalice la carga, simplemente comienza a cargar los datos de la base de datos. Para cuando loadModelWithDataFromFirebase()regrese su función, la carga aún no ha terminado.

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

Al contrario de lo que probablemente esperaría, 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:

  1. Aplastar el error asincrónico (normalmente acompañado de murmullos de frases como: "fue un error, esta gente no sabe lo que está haciendo")

  2. abrazar a la bestia asincrónica (normalmente acompañada de bastantes horas de maldiciones, pero después de un tiempo de paz y aplicaciones con mejor comportamiento)

Tome la píldora azul: haga que la llamada asincrónica se comporte sincrónicamente

Si tiene ganas de elegir la primera opción, una primitiva de sincronización bien ubicada será suficiente:

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 error) { throw error.toException(); }
    });
    semaphore.acquire();
    setBookmarks(loadedBookmarks);
}

Actualización (20160303) : cuando probé esto en Android, bloqueó mi aplicación. Funciona bien en una JVM normal, pero Android es más delicado cuando se trata de subprocesos. Siéntete libre de intentar hacerlo funcionar... o

Tome la píldora roja: enfrente la naturaleza asincrónica de la sincronización de datos en Firebase

Si, en cambio, elige adoptar la programación asincrónica, debe reconsiderar 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íncrona, debería pensar como "Siempre que se hayan cargado los marcadores, quiero cargar los datos de muestra. Siempre que se hayan cargado 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 cambian constantemente y, por lo tanto, se sincronizan varias veces: "Cada vez que cambian los marcadores, quiero cargar también los datos de muestra. Cada vez que cambian los datos de muestra, quiero cargar incluso más."

En 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 error) { throw error.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 onDataChangeejecuta y (re)cargamos 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 desees definir tu propia interfaz de devolución de llamada, en lugar de llamar al código preciso en onDataChange. Eche un vistazo a esta respuesta para ver un buen ejemplo de ello.

Frank van Puffelen avatar Oct 19 '2015 00:10 Frank van Puffelen

TL;DR: Adopte la asincronicidad de Firebase

Como mencioné en otra publicación , 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

La API de Google para Android proporciona un marco de tareas (tal como lo hizo Parse con Bolts ), que es similar al concepto de promesas de JavaScript .

Primero crea un archivo Taskpara descargar el marcador desde 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 tienes la idea, supón que setBookmarksy loadSampleDatatambién son asincrónicos. También puedes crearlas como Continuationtareas (como 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) {
        // ...
    }
}
JP Ventura avatar Nov 12 '2016 14:11 JP Ventura