roble 1.9.0 copy "roble: ^1.9.0" to clipboard
roble: ^1.9.0 copied to clipboard

Cliente Flutter para la plataforma ROBLE de Uninorte OpenLab. Autenticación y CRUD sobre bases de datos PostgreSQL.

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:

  • id y userId no son lo mismo. userId es el del usuario, el que guardas en tus tablas para saber de quién es cada fila. id es el de la fila del perfil.
  • role puede ser null, 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 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 chrome elige 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=5001

Y localhost no es 127.0.0.1 para 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.


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');

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.


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
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 — la tabla no es pública (en publicRead).
  • 404 — no existe esa consulta guardada.

Referencia rápida #

Todos los métodos son asíncronos salvo isLoggedIn e isSocialCallback.

Sesión #

Método Devuelve
isLoggedIn bool — si hay sesión en memoria
restoreSession() booltrue 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() RobleInsertResultinserted 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
publicRead() List<Map> — sin necesidad de sesión
executeQuery() RobleQueryResultrows, 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() Stringla clave que generó el servidor
json.remove() nada
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).


Licencia #

MIT

0
likes
140
points
455
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Cliente Flutter para la plataforma ROBLE de Uninorte OpenLab. Autenticación y CRUD sobre bases de datos PostgreSQL.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

crypto, flutter, flutter_secure_storage, flutter_web_auth_2, google_sign_in, http, socket_io_client, web

More

Packages that depend on roble