roble
Cliente de Flutter para Roble, la plataforma de Uninorte OpenLab.
Con este paquete tu app puede tener cuentas de usuario, guardar datos y enterarse de los cambios al momento, sin que escribas backend.
Instalación
flutter pub add roble
En Android, abre android/app/src/main/AndroidManifest.xml y añade permiso de
internet dentro de <manifest>:
<uses-permission android:name="android.permission.INTERNET" />
Tu primer minuto
Necesitas dos datos de la consola de Roble: la URL y el id de tu proyecto (el «contrato»).
import 'package:roble/roble.dart';
final db = RobleApiDataBase(
config: RobleApiConfig.fromContract(
baseUrl: 'https://roble-api.test-openlab.uninorte.edu.co',
contractId: 'miproyecto_ab12cd34',
),
);
// Crear una cuenta
await db.register(
email: 'ana@correo.com',
password: 'MiClave!1',
name: 'Ana García',
);
// Entrar. Devuelve el perfil.
final usuario = await db.login(
email: 'ana@correo.com',
password: 'MiClave!1',
);
print('Hola ${usuario['name']}');
Crea db una sola vez en tu app y reutilízalo. Si lo creas de nuevo en
cada pantalla, cada copia tendrá su propia sesión.
Cuentas de usuario
Entrar y salir
await db.login(email: correo, password: clave);
await db.logout();
if (db.isLoggedIn) print('Hay alguien dentro');
final perfil = await db.currentUser();
Qué devuelve el login
login(), signInWithGoogle() y cualquier otra forma de entrar devuelven
el mismo mapa: el perfil de la persona.
{
'id': 'us-3f2a…', // fila del perfil
'userId': '9c1e…', // el usuario. Este es el que referencian tus tablas
'email': 'ana@correo.com',
'name': 'Ana García',
'role': 'admin', // null si no tiene rol asignado
'extra': {'programa': 'Sistemas'},
'createdAt': '2026-08-27T12:00:00.000Z',
'updatedAt': null,
}
Dos avisos:
idyuserIdno son lo mismo.userIdes el del usuario, el que guardas en tus tablas para saber de quién es cada fila.ides el de la fila del perfil.rolepuede sernull, si nadie le asignó rol. No es un error.
currentUser() devuelve exactamente esto mismo, y es lo que usas después de
restoreSession() para saber quién entró.
Que la sesión sobreviva a cerrar la app
Por omisión la sesión se guarda en el almacenamiento seguro del teléfono. Al arrancar, pregúntale a Roble si sigue siendo válida:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
if (await db.restoreSession()) {
// Entra directo a la pantalla principal
} else {
// Muestra el login
}
runApp(MiApp());
}
Registro con código por correo
Si prefieres confirmar que el correo existe antes de crear la cuenta:
await db.registerWithVerification(
email: 'ana@correo.com',
password: 'MiClave!1',
name: 'Ana García',
);
// Ana recibe un código y lo escribe en tu pantalla
await db.verifyEmail(email: 'ana@correo.com', code: '123456');
resendCode(email: ...) lo manda otra vez si no llegó.
Contraseña olvidada
await db.forgotPassword(email: 'ana@correo.com');
// Le llega un código por correo
await db.resetPassword(token: '123456', newPassword: 'OtraClave!2');
Entrar sin cuenta
Dos formas, y la diferencia importa: una deja dueño, la otra no.
Invitado: escribe ahora, se registra después
if (!db.isLoggedIn) await db.signInAnonymously();
await db.create('carrito', {'producto': id}); // suyo, y de nadie más
Un invitado es un usuario de verdad. Tiene userId, cada fila que inserta
queda a su nombre, y isMine() y el alcance own funcionan igual que con una
cuenta normal. Lo único que no tiene es correo y contraseña.
db.isAnonymous responde desde el token, sin ir al servidor, que es lo que
necesitas para decidir si la pantalla enseña «guarda tu cuenta».
Su email es una dirección inventada anon_…@anonymous.invalid. No existe y
no puede recibir correo: no la muestres en pantalla.
Cuando quiera conservarlo:
try {
await db.upgradeAccount(email: email, password: password);
} on RobleAnonUpgradeEmailTakenException {
// ya hay cuenta con ese correo: ofrécele iniciar sesión, avisando
// de que lo escrito como invitado se queda en la sesión de invitado
}
Esto no crea un usuario nuevo: muta el que ya hay. El userId no cambia,
así que todo lo que escribió sigue siendo suyo sin mover un solo dato. Ese es
el punto entero.
Con Google o Microsoft es lo mismo, con linkIdentity(provider: 'google'):
devuelve la url a la que mandar a la persona, y al volver la identidad queda
unida a esta cuenta en vez de crear otra.
El proyecto tiene que tener habilitado el acceso anónimo y aplicar
propiedad por fila en alguna tabla. Si falta algo sale
RobleAnonymousAuthException y su code dice cuál de las dos cosas. La
segunda condición no es un capricho: un invitado sin propiedad por fila escribe
filas que puede borrar cualquier otro invitado.
Clave publicable: un buzón, sin sesión ninguna
Para un formulario de contacto, una encuesta, un buzón de sugerencias: gente que no se va a registrar y que no necesita volver a ver lo que mandó.
final buzon = RobleApiDataBase(
config: RobleApiConfig.fromContract(
baseUrl: baseUrl,
contractId: contractId,
anonKey: 'roble_anon_…', // se emite en la consola del proyecto
),
);
await buzon.create('sugerencias', {'texto': texto});
La clave va dentro de la app y es pública por diseño: quien desensamble el binario la va a ver, y eso no es una filtración. Lo que la hace segura es que no puede leer nada.
Un cliente así sólo inserta, y sólo en las tablas que el proyecto haya marcado. Cualquier otra cosa falla en tu código, sin salir a la red:
await buzon.read('sugerencias');
// RobleAnonKeyScopeException: una clave publicable sólo puede insertar
buzon.isAnonKeyMode es true, para no ofrecer en pantalla un botón que
siempre va a fallar.
Las filas que escribe no tienen dueño. Nadie con alcance own va a poder
editarlas ni borrarlas después. Si la persona tiene que poder volver a tocar lo
suyo, lo que quieres es la sesión de invitado de arriba, no esto.
Entrar con Google
Una línea:
final usuario = await db.signInWithGoogle();
En móvil sale el selector de cuentas del teléfono. En web se abre una ventana. El paquete elige solo.
Antes tienes que configurar Google en la consola de Roble y registrar allí el «destino de retorno» de tu app. En iOS, además, pasa el Client ID de iOS al crear el cliente:
RobleApiDataBase(
config: config,
googleIosClientId: 'xxxx.apps.googleusercontent.com',
ssoRedirect: 'mi-app-web', // el nombre que registraste en la consola
);
Un destino de retorno por entorno
ssoRedirect no es una URL: es el nombre de un destino registrado en la
consola. La URL vive allí, así que puedes tener varios y elegir cuál usa cada
build.
Registra uno por entorno en vez de irle cambiando la URL al mismo:
| Nombre en la consola | URL |
|---|---|
mi-app-web-dev |
http://localhost:5001 |
mi-app-web |
https://mi-dominio |
mi-app-movil |
com.miempresa.miapp://sso-done |
Así desarrollo y producción no se pelean por el mismo sitio, y puedes probar el flujo real en local sin tocar lo que usan los demás. Publicar deja de ser un cambio en la consola: es otro valor en la configuración de ese build.
En local, el puerto forma parte del destino.
flutter run -d chromeelige un puerto distinto en cada arranque si no se lo fijas, así que Google autentica bien y te devuelve a un puerto donde ya no hay nadie:ERR_CONNECTION_REFUSED, que parece un fallo del login y no lo es.flutter run -d chrome --web-port=5001Y
localhostno es127.0.0.1para el proveedor, aunque sean la misma máquina: abre la app por el mismo origen que registraste.
Fuera de web el destino es el esquema propio de la app, declarado en
AndroidManifest.xml y en Info.plist. No lleva dominio, así que no cambia al
publicar.
¿Qué proveedores tienes activos? Para pintar solo los botones que funcionan:
for (final p in await db.listProviders()) {
print(p.displayName); // "Google", "Microsoft"…
}
Guardar datos: tablas
Una tabla es como una hoja de cálculo: la creas en la consola con sus columnas, y desde la app la llenas.
// Crear
final producto = await db.create('Product', {
'name': 'Café',
'quantity': 12,
});
// Leer todo
final todos = await db.read('Product');
// Leer con filtro (igualdad)
final agotados = await db.read('Product', filters: {'quantity': 0});
// Uno solo, por su id
final uno = await db.getById('Product', producto['_id']);
// Cambiar
await db.update('Product', producto['_id'], {'quantity': 11});
// Borrar
await db.delete('Product', producto['_id']);
Cada registro trae un _id que pone Roble. Es lo que usas para cambiarlo o
borrarlo.
Varios de golpe
final res = await db.createMany('Product', [
{'name': 'Té', 'quantity': 5},
{'name': 'Pan', 'quantity': 0},
]);
print('Guardados: ${res.inserted.length}, rechazados: ${res.skipped.length}');
Consultas más complicadas
read solo filtra por igualdad. Para juntar tablas, sumar o paginar, guarda la
consulta SQL en la consola y llámala por su nombre:
final res = await db.executeQueryByName('productosSinInventario');
for (final fila in res.rows) print(fila);
Usa el nombre, no el UUID: el nombre sobrevive si recreas la consulta.
Una tabla que todos pueden leer
Si marcas una tabla como pública en la consola, se puede leer sin haber iniciado sesión:
final catalogo = await db.publicRead('Product');
Ojo: público es público. Cualquiera con el id del proyecto puede leerla.
Quién puede tocar qué
Los permisos de Roble son por tabla y por acción. Un rol puede leer
Product y no poder borrarla; darle permiso de borrado en una tabla no se lo da
en las demás. Cuando no lo tiene, la llamada responde 403:
try {
await db.delete('Product', id);
} on RobleApiForbiddenException {
mostrar('Tu cuenta no puede borrar productos');
}
Los roles que trae un proyecto nuevo:
| Rol | Qué puede |
|---|---|
admin |
Todo, sobre cualquier fila |
user |
Crear y leer (es el rol de quien se registra) |
editor |
Crear, leer, cambiar y borrar cualquier fila |
editor_own |
Lo mismo, pero sólo sobre sus propias filas |
editor_own es el que suele hacer falta: «que cada quien edite lo suyo» sin que
promover a alguien le deje vaciar la tabla. El rol de la sesión viene en
RobleUser.role, y están escritos en RobleRole para no teclearlos:
final user = await db.currentUser();
if (user.role == RobleRole.editorOwn) mostrarBotonEditar();
De quién es cada fila
Las tablas creadas desde que existe la propiedad por fila traen una columna
_owner con el usuario que insertó el registro. La pone el servidor: lo que
mandes tú en ella no se respeta ni al crear ni al actualizar, y este paquete la
quita de los datos que envías, así que leer una fila, cambiarle algo y volver a
escribirla sigue funcionando.
Para pintar «esto es mío» no hace falta ir al servidor:
final filas = await db.read('Product');
final mias = filas.where(db.isMine).toList();
// El id de la sesión, si lo prefieres a mano
db.currentUserId;
isMine mira _owner contra la sesión: es para la pantalla, no una
comprobación de seguridad — quien decide qué puedes tocar es el servidor.
Una tabla puede ocultar _owner desde la consola (expose_owner), que es
lo que quieres en una tabla anónima: ahí no viene en las lecturas, filtrar por
ella da error, y isMine responde false porque no hay forma de saberlo.
Cuando la tabla sólo te deja lo tuyo
Con la propiedad activada en una tabla, tocar la fila de otra persona responde
404, el mismo que si no existiera. Es a propósito: si respondiera distinto,
probar identificadores diría cuáles existen y de quién son.
try {
await db.delete('Product', id);
} on RobleApiNotFoundException {
mostrar('Ese producto ya no está, o no es tuyo');
}
Un cambio a tener en cuenta: borrar dos veces el mismo _id ya no responde
200, responde 404. Si tu app reintenta borrados, trata ese 404 como
éxito.
Guardar datos: árbol JSON
A veces no vale la pena declarar una tabla: un chat, un tablero, una partida. Para eso está el árbol JSON. No declaras nada: la estructura nace cuando escribes el primer dato.
// Añadir, con clave que genera el servidor
final id = await db.json.push('mensajes', {
'texto': 'hola',
'de': 'ana@correo.com',
});
// Leer la colección entera
final todos = await db.json.read('mensajes');
// Cambiar solo una clave
await db.json.update('mensajes/$id', {'leido': true});
// Borrar
await db.json.remove('mensajes/$id');
Borra ramas, no colecciones: json.remove('mensajes') —un solo trozo— vacía la
colección entera y es cosa de administradores, así que a un usuario normal
le responde RobleApiForbiddenException. Con json.remove('mensajes/$id') no
hace falta ningún rol.
Una colección puede además exigir que la ruta sea tuya: si la consola marca uno
de sus segmentos como el del dueño (por ejemplo mensajes/<tuId>/...), escribir
o borrar por debajo del segmento de otra persona es un 403, y suscribirse a lo
que no puedes leer lanza un error de permisos en el stream en vez de emitir.
Una ruta es coleccion/hijo/nieto. El primer trozo es la colección.
Las claves de push salen ordenadas por tiempo, así que ordenarlas ordena los
mensajes — sin depender del reloj de cada teléfono.
¿Tabla o árbol JSON?
| Usa una tabla cuando | Usa el árbol JSON cuando |
|---|---|
| Los datos tienen forma fija | La forma cambia o no importa |
| Quieres consultas SQL | Solo lees y escribes por ruta |
| Son datos del negocio | Son datos que van y vienen |
Enterarse de los cambios al momento
Escuchar te avisa cuando otro usuario cambia algo, sin que tengas que recargar.
// Una tabla
final sub = db.watchTable('Product').listen((cambio) {
print('${cambio.type}: ${cambio.record}');
});
// Un solo registro
db.watchRecord('Product', id).listen((cambio) { ... });
// El árbol JSON
db.json.watch('mensajes').listen((cambio) {
// En un push, `record` trae {claveNueva: dato}
cambio.record?.forEach((id, dato) => print(dato));
});
// Al salir de la pantalla
await sub.cancel();
Tres cosas que conviene saber:
- No trae lo que ya existe, solo lo que cambie de ahora en adelante. Para pintar la lista, léela primero y aplica encima lo que llegue.
- Cancela al salir de la pantalla. Si no, el socket sigue abierto.
- Hace falta sesión iniciada.
Puedes pedir solo algunos cambios, o filtrar en el servidor:
db.watchTable(
'Product',
events: [RobleChangeType.insert],
filters: [RobleFilter('quantity', 'eq', 0)],
).listen(...);
Filtrar aquí ahorra el viaje de todo lo que no te interesa.
Notificaciones
Avisos que se guardan y llegan al momento a quien tenga la app abierta. Es otra cosa que el árbol JSON: no hay colección que crear, no hay ruta que elegir, y el destinatario es un usuario del proyecto.
// Escuchar lo que vaya llegando.
db.notifications.watch().listen((evento) {
if (evento.type == RobleNotificationEventType.created) {
mostrarAviso(evento.notification.title);
}
});
// Enviar a alguien.
await db.notifications.send(
to: otroUsuarioId,
title: 'Te toca',
body: 'Ana movió ficha',
topic: 'partida',
data: {'partidaId': '42'},
);
El data es tuyo: lo que la pantalla necesite para abrir lo correcto cuando
alguien toque el aviso.
A todo el proyecto
await db.notifications.send(
to: robleNotificationEveryone,
title: 'Mañana no hay clase',
);
Llega a todos. Cada persona la marca leída por su cuenta: que tú la leas no la marca para los demás. Una de proyecto no se puede borrar desde la app —la notificación es una sola y es de todos—, se marca leída y ya.
Canales: avisar a un grupo
Cuando el aviso no es para una persona ni para todo el proyecto —los de un curso, los que siguen un tema— eso es un canal. Se envía una vez y lo reciben quienes estén dentro.
await db.notifications.subscribe('curso-101');
final mios = await db.notifications.channels();
// Enviar: `channel` en vez de `to`.
await db.notifications.send(channel: 'curso-101', title: 'Examen el viernes');
await db.notifications.unsubscribe('curso-101');
Un canal existe con solo usarlo; no hay que crearlo. Lo que sí se configura desde la consola es quién puede entrar:
open |
Cualquiera se suscribe solo. Para «me interesa el fútbol». |
roles |
Solo ciertos roles. |
managed |
La membresía la pone el servidor. Para una matrícula: quién está en Cálculo II no lo decide el estudiante. |
Dos cosas que conviene saber: al entrar no recibes lo anterior —te enteras de
lo que pasa desde que entras— y salirte siempre puedes, también de un canal
managed.
Lo que ya estaba ahí
El stream no trae lo anterior, solo lo que llegue a partir de ahora. Para pintar la lista al abrir:
final lista = await db.notifications.list(); // las 50 últimas
final sinLeer = await db.notifications.list(unread: true);
final cuantas = await db.notifications.unreadCount(); // el número del globito
list() acepta topic, limit (1-100) y before para ir hacia atrás.
Marcarlas
await db.notifications.markRead(notificacion.id);
await db.notifications.markAllRead();
await db.notifications.remove(notificacion.id); // solo las dirigidas a ti
Marcar una vuelve por el stream como un evento read, y solo a tus
dispositivos: el teléfono se entera de lo que marcaste en el navegador.
El globito, sin pedirlo
Al conectar, el servidor ya manda cuántas hay sin leer:
db.notifications.unreadCountChanges.listen((n) => setState(() => badge = n));
Enviar más tarde, o todos los días
Una notificación se puede dejar programada: la manda el servidor cuando llegue la hora, aunque nadie tenga la app abierta.
// Un recordatorio, una sola vez.
await db.notifications.schedule(
to: usuarioId,
title: 'Tu cita es en una hora',
at: DateTime.now().add(const Duration(hours: 1)),
);
// Un "buenos días" todos los días, sin montar un cron.
await db.notifications.schedule(
to: robleNotificationEveryone,
title: '¡Buenos días!',
at: manana7am,
repeat: RobleRepeat.daily,
);
final pendientes = await db.notifications.scheduled();
await db.notifications.cancelScheduled(pendientes.first.id);
La fecha se manda siempre en UTC, así que puedes pasar una hora local sin
pensarlo. Solo ves las tuyas. Quien puede enviar también decide quién puede
programar, y se comprueba dos veces: al programar —para que te enteres ahora— y
al enviar, porque para entonces la política del proyecto puede haber cambiado.
Si ya no puedes, queda en failed con el motivo en lastError, y una repetida
que falla se para en vez de reintentar cada día.
Que lleguen con la app cerrada (push)
Lo de arriba llega mientras la app está abierta. Para que suene con la app cerrada hace falta Firebase, y hacen falta tus credenciales, no las de Roble: un token de FCM está atado al proyecto de Firebase con el que registraste tu app, así que nadie puede enviarle push en tu nombre.
Una vez, en la consola de Roble: sección Notificaciones → Push, sube el JSON de la cuenta de servicio de tu proyecto de Firebase (Configuración del proyecto → Cuentas de servicio → Generar nueva clave privada).
Después, en tu app. El token lo da firebase_messaging, que va en tu app y no
en este paquete —así una app que no quiera push no carga Firebase—:
final token = await FirebaseMessaging.instance.getToken();
if (token != null) {
await db.notifications.registerDevice(token, RobleDevicePlatform.android);
}
Llámalo en cada arranque: repetirlo con el mismo token no duplica nada, solo
refresca la fecha. Y al cerrar sesión, antes de logout():
await db.notifications.unregisterDevice(token);
Si no lo sueltas, ese aparato sigue recibiendo los avisos de esa cuenta.
El push es un segundo camino, no el principal: la notificación se guarda y llega por el stream igual aunque Firebase no esté configurado o falle.
Ojo
Cualquiera con sesión en el proyecto puede enviar a cualquiera, igual que cualquiera puede escribir en cualquier rama del árbol JSON. Si el aviso lo tiene que mandar solo el profesor, esa comprobación va en tu código.
Cuando algo falla
Todo lanza alguna subclase de RobleApiException:
try {
await db.login(email: correo, password: clave);
} on RobleApiHttpException catch (e) {
if (e.statusCode == 401) mostrar('Correo o contraseña incorrectos');
} on RobleApiNetworkException {
mostrar('Sin conexión');
} on RobleApiException catch (e) {
mostrar(e.message);
}
| Excepción | Qué pasó |
|---|---|
RobleApiHttpException |
El servidor respondió con error. Mira statusCode |
RobleApiForbiddenException |
403: tu rol no puede hacer eso (subclase de la anterior) |
RobleApiNotFoundException |
404: no existe, o no es tuyo (subclase de la anterior) |
RobleApiNetworkException |
No se pudo llegar al servidor |
RobleApiTimeoutException |
Tardó demasiado |
RobleApiAuthException |
Problema de sesión o de login social |
RobleApiFormatException |
La respuesta no tenía la forma esperada |
RobleApiConflictException |
Ya existe una cuenta con ese correo |
Los números que más vas a ver:
- 401 — no hay sesión, o las credenciales están mal.
- 403 — tu rol no puede hacer eso sobre esa tabla, la tabla no es pública
(en
publicRead), o intentaste borrar una colección entera del árbol JSON, que es cosa de administradores. - 404 — la fila no existe o no es tuya, o no existe esa consulta guardada.
Las dos últimas tienen su propia clase, así que no hace falta comparar números:
RobleApiForbiddenException y RobleApiNotFoundException. Ambas siguen siendo
RobleApiHttpException, así que el código que ya miraba statusCode no
cambia.
Referencia rápida
Todos los métodos son asíncronos salvo isLoggedIn, isSocialCallback,
currentUserId e isMine.
Sesión
| Método | Devuelve |
|---|---|
isLoggedIn |
bool — si hay sesión en memoria |
restoreSession() |
bool — true si la sesión guardada sigue viva |
logout() |
nada |
Cuentas
| Método | Devuelve |
|---|---|
register() |
el usuario creado, tal como lo mandó el servidor |
registerWithVerification() |
confirmación de que el correo salió |
verifyEmail() |
confirmación |
resendCode() |
confirmación |
login() |
el perfil (arriba) |
currentUser() |
el perfil |
forgotPassword() |
confirmación de que el correo salió |
resetPassword() |
confirmación |
deleteAccount() |
nada |
Login social
| Método | Devuelve |
|---|---|
signInWithGoogle() |
el perfil |
signInWithProvider() |
el perfil |
signInWithIdToken() |
el perfil |
exchangeSocialCode() |
el perfil |
listProviders() |
List<RobleProviderInfo> — name, displayName, clientId, autoLinkSupported |
providerClientId() |
String? — null si ese proveedor no está configurado |
startSocialLogin() |
Uri — a dónde mandar a la persona |
isSocialCallback() |
bool |
RobleApiDataBase.newNonce() |
String |
Tablas
| Método | Devuelve |
|---|---|
create() |
el registro creado, con su _id |
createMany() |
RobleInsertResult — inserted y skipped |
read() |
List<Map> — vacía si no hay nada, nunca null |
getById() |
Map? — null si no existe |
update() |
el registro ya cambiado |
delete() |
confirmación del borrado — 404 si ya no estaba o no es tuyo |
isMine() |
bool — si la fila lleva tu _owner |
currentUserId |
String? — el id de la sesión, sin ir al servidor |
publicRead() |
List<Map> — sin necesidad de sesión |
executeQuery() |
RobleQueryResult — rows, rowCount, fields |
executeQueryByName() |
RobleQueryResult |
Árbol JSON
| Método | Devuelve |
|---|---|
json.collections() |
List<String> — los nombres |
json.read() |
lo que haya en esa ruta, o null si no existe |
json.write() |
nada |
json.update() |
nada |
json.push() |
String — la clave que generó el servidor |
json.remove() |
nada — con un solo segmento, sólo para admin |
json.watch() |
Stream<RobleChange> |
Tiempo real
| Método | Devuelve |
|---|---|
watchTable() |
Stream<RobleChange> |
watchRecord() |
Stream<RobleChange> |
realtimePolicies() |
List<RobleTablePolicy> |
realtimePolicy() |
RobleTablePolicy? — null si esa tabla no tiene política |
setRealtimePolicy() |
la política ya guardada |
disableRealtime() |
nada |
Un RobleChange trae type (insert, update, delete), table,
record (la fila después del cambio, null al borrar), previous,
primaryKey, id, commitTimestamp y path (solo en el árbol JSON).
Notificaciones
| Método | Devuelve |
|---|---|
notifications.send() |
las notificaciones creadas, una por destinatario |
notifications.list() |
List<RobleNotification>, de la más reciente a la más antigua |
notifications.unreadCount() |
int |
notifications.markRead() |
la notificación ya marcada |
notifications.markAllRead() |
int — cuántas cambiaron |
notifications.remove() |
nada |
notifications.watch() |
Stream<RobleNotificationEvent> |
notifications.unreadCountChanges |
Stream<int> |
notifications.subscribe() |
nada |
notifications.unsubscribe() |
nada |
notifications.channels() |
List<RobleChannel> — en los que estás |
notifications.schedule() |
el envío programado |
notifications.scheduled() |
List<RobleScheduledNotification> — los tuyos |
notifications.cancelScheduled() |
nada |
notifications.registerDevice() |
el aparato apuntado |
notifications.unregisterDevice() |
nada |
notifications.devices() |
List<RobleDevice> — los aparatos de esta cuenta |
Una RobleNotification trae id, title, body, topic, data,
recipientId (* si es de proyecto, o isForEveryone), senderId, readAt
(o isUnread), createdAt y expiresAt.
Licencia
MIT