¿Cómo devolver el valor de DataSnapshot como resultado de un método?
No tengo mucha experiencia con Java. No estoy seguro de si esta pregunta es estúpida, pero necesito obtener un nombre de usuario de la base de datos en tiempo real de Firebase y devolver este nombre como resultado de este método. Entonces, descubrí cómo obtener este valor, pero no entiendo cómo devolverlo como resultado de este método. ¿Cuál es la mejor manera de hacer esto?
private String getUserName(String uid) {
databaseReference.child(String.format("users/%s/name", uid))
.addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
// How to return this value?
dataSnapshot.getValue(String.class);
}
@Override
public void onCancelled(DatabaseError databaseError) {}
});
}
Este es un problema clásico con las API web asincrónicas. No puedes devolver algo ahora que aún no se haya cargado. En otras palabras, no puede simplemente crear una variable global y usarla fuera onDataChange()
del método porque siempre lo será null
. Esto sucede porque onDataChange()
el método se llama asíncrono. Dependiendo de la velocidad de su conexión y del estado, pueden pasar desde unos cientos de milisegundos hasta unos segundos antes de que los datos estén disponibles.
Pero no sólo Firebase Realtime Database carga datos de forma asincrónica, sino que casi todas las API web modernas también lo hacen, ya que puede llevar algún tiempo. Entonces, en lugar de esperar los datos (lo que puede provocar que los cuadros de diálogo de la aplicación no respondan a sus usuarios), el código de su aplicación principal continúa mientras los datos se cargan en un subproceso secundario. Luego, cuando los datos están disponibles, se llama a su método onDataChange() y puede usar los datos. En otras palabras, cuando onDataChange()
se llama al método, sus datos aún no están cargados.
Tomemos un ejemplo, colocando algunas declaraciones de registro en el código, para ver más claramente lo que está sucediendo.
private String getUserName(String uid) {
Log.d("TAG", "Before attaching the listener!");
databaseReference.child(String.format("users/%s/name", uid)).addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
// How to return this value?
dataSnapshot.getValue(String.class);
Log.d("TAG", "Inside onDataChange() method!");
}
@Override
public void onCancelled(DatabaseError databaseError) {}
});
Log.d("TAG", "After attaching the listener!");
}
Si ejecutamos este código, el resultado será:
¡Antes de adjuntar al oyente!
¡Después de adjuntar al oyente!
¡Dentro del método onDataChange()!
Probablemente esto no sea lo que esperabas, pero explica precisamente por qué tus datos están null
al devolverlos.
La respuesta inicial de la mayoría de los desarrolladores es intentar "arreglar" este problema asynchronous behavior
, lo cual personalmente recomiendo. La web es asincrónica y cuanto antes lo acepte, antes podrá aprender cómo ser productivo con las API web modernas.
Me ha resultado más fácil replantear los problemas de este paradigma asincrónico. En lugar de decir "Primero obtenga los datos, luego regístrelos", planteo el problema como "Empiece a obtener datos. Cuando se carguen los datos, regístrelos". Esto significa que cualquier código que requiera datos debe estar dentro del onDataChange()
método o llamarse desde allí, como este:
databaseReference.child(String.format("users/%s/name", uid)).addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
// How to return this value?
if(dataSnapshot != null) {
System.out.println(dataSnapshot.getValue(String.class));
}
}
@Override
public void onCancelled(DatabaseError databaseError) {}
});
Si desea utilizar eso en el exterior, existe otro enfoque. Debes crear tu propia devolución de llamada para esperar a que Firebase te devuelva los datos. Para lograr esto, primero debe crear un archivo interface
como este:
public interface MyCallback {
void onCallback(String value);
}
Luego necesita crear un método que realmente obtenga los datos de la base de datos. Este método debería verse así:
public void readData(MyCallback myCallback) {
databaseReference.child(String.format("users/%s/name", uid)).addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
String value = dataSnapshot.getValue(String.class);
myCallback.onCallback(value);
}
@Override
public void onCancelled(DatabaseError databaseError) {}
});
}
Al final, simplemente llame al readData()
método y pase una instancia de la MyCallback
interfaz como argumento donde lo necesite, de esta manera:
readData(new MyCallback() {
@Override
public void onCallback(String value) {
Log.d("TAG", value);
}
});
Esta es la única forma en que puede utilizar ese valor fuera onDataChange()
del método. Para obtener más información, también puedes echar un vistazo a este vídeo .
Editar: 26 de febrero de 2021
Para más información, puedes consultar el siguiente artículo:
- ¿Cómo leer datos de Firebase Realtime Database usando get()?
Y el siguiente vídeo:
- https://youtu.be/mOB40wowo6Y
A partir de la versión "19.6.0" , el SDK de Firebase Realtime Database contiene un nuevo método llamado get(), que se puede invocar en una DatabaseReference o en un objeto Query:
Se agregaron DatabaseReference#get() y Query#get(), que devuelven datos del servidor incluso cuando hay datos más antiguos disponibles en el caché local.
Como ya sabemos, la API de Firebase es asíncrona. Entonces necesitamos crear una devolución de llamada para eso. Primero, creemos una interfaz:
public interface FirebaseCallback {
void onResponse(String name);
}
Y un método que toma como argumento un objeto de tipo FirebaseCallback:
public void readFirebaseName(FirebaseCallback callback) {
DatabaseReference uidRef = databaseReference.child(String.format("users/%s/name", uid))
uidRef.get().addOnCompleteListener(new OnCompleteListener<DataSnapshot>() {
@Override
public void onComplete(@NonNull Task<DataSnapshot> task) {
if (task.isSuccessful()) {
String name = task.getResult().getValue(String.class);
callback.onResponse(name);
} else {
Log.d(TAG, task.getException().getMessage());
}
}
});
}
Ahora, para leer los datos, simplemente debe llamar al método anterior pasando como argumento un objeto de tipo FirebaseCallback:
readFirebaseName(new FirebaseCallback() {
@Override
public void onResponse(String name) {
Log.d("TAG", name);
}
});
Para más información, puedes consultar el siguiente artículo:
- ¿Cómo leer datos de Firebase Realtime Database usando get()?
Y el siguiente vídeo:
- https://youtu.be/mOB40wowo6Y
Creo que entiendo lo que preguntas. Aunque diga que desea "devolverlo" (per se) desde el método de recuperación, puede ser suficiente decir que en realidad sólo desea poder utilizar el valor recuperado una vez que se haya completado la recuperación. Si es así, esto es lo que debes hacer:
- Crea una variable en la parte superior de tu clase.
- Recupera tu valor (lo que has hecho en su mayor parte correctamente)
- Establezca la variable pública en su clase igual al valor recuperado
Una vez que la recuperación sea exitosa, podrá hacer muchas cosas con la variable. 4a y 4b son algunos ejemplos sencillos:
4a. Editar:
como ejemplo de uso, puede activar cualquier otra cosa que necesite ejecutar en su clase que use yourNameVariable
(y puede estar seguro de que yourNameVariable
no es nula)
4b. Editar: como ejemplo de uso, puede usar la variable en una función que se activa mediante onClickListener de un botón.
Prueba esto.
// 1. Create a variable at the top of your class
private String yourNameVariable;
// 2. Retrieve your value (which you have done mostly correctly)
private void getUserName(String uid) {
databaseReference.child(String.format("users/%s/name", uid))
.addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
// 3. Set the public variable in your class equal to value retrieved
yourNameVariable = dataSnapshot.getValue(String.class);
// 4a. EDIT: now that your fetch succeeded, you can trigger whatever else you need to run in your class that uses `yourNameVariable`, and you can be sure `yourNameVariable` is not null.
sayHiToMe();
}
@Override
public void onCancelled(DatabaseError databaseError) {}
});
}
// (part of step 4a)
public void sayHiToMe() {
Log.d(TAG, "hi there, " + yourNameVariable);
}
// 4b. use the variable in a function triggered by the onClickListener of a button.
public void helloButtonWasPressed() {
if (yourNameVariable != null) {
Log.d(TAG, "hi there, " + yourNameVariable);
}
}
Luego, podrás usarlo yourNameVariable
donde quieras durante tu clase.
Nota: solo asegúrese de verificar que yourNameVariable
no sea nulo cuando lo use, ya que onDataChange
es asincrónico y es posible que no se haya completado en el momento en que intenta usarlo en otro lugar.