DEV Community

Cover image for Registro 004-1. Domina Flutter con Clean Architecture: Integración de Supabase y Repositorios Simplificada
JR Saucedo
JR Saucedo

Posted on

Registro 004-1. Domina Flutter con Clean Architecture: Integración de Supabase y Repositorios Simplificada

Continuando con el progreso de nuestro proyecto, ya sabemos cómo se vería la configuración inicial de nuestro service locator para la inyección de dependencias. Ahora, vayamos a algo menos estándar y más relacionado con las características específicas del proyecto, donde comenzaremos a aplicar la clean architecture.


Cuando iniciamos un proyecto y aún no contamos con toda la estructura clara de cómo será, lo que sí solemos tener en mente es nuestro proceso de autenticación. Aunque en nuestro caso sabemos hacia dónde vamos, en este proyecto utilizaremos Supabase y sus capacidades de control de acceso.

features/user

Primero, creamos nuestros directorios a utilizar:

Estructura

Aquí es donde comenzamos a aplicar nuestra clean architecture, dividiendo los directorios y sus responsabilidades. Personalmente, me gusta comenzar en orden alfabético la creación de archivos, así que empecemos por la fuente de datos: data, donde manejaremos todo lo relacionado con la obtención de información del usuario. Primero crearemos el modelo, no muy alfabéticamente de mi parte, pero luego entenderán por qué.

features/user/data/models

Aquí definimos lo que esperamos recibir de nuestra instancia de Supabase para el perfil de usuario:

Modelo user

Desglosemos lo que tenemos aquí:

  • Usamos la anotación @freezed, que nos ayudará a crear todos los métodos necesarios para nuestro modelo, cumpliendo con los estándares de Dart, aunque no se usen todos.
  • Declaramos nuestra clase y la unimos a nuestro mixin con todos estos métodos.
  • En nuestro primer factory, definimos los campos que tendrá nuestro modelo.
  • Por último, creamos un factory fromJson, útil para transformar datos en formato JSON (de APIs, SDKs, etc.) a una data class compatible con Dart, que se generará automáticamente gracias a la anotación @freezed.

Modelo recién creado

Al crear el modelo, verás que el documento se llena de errores, pero no pasa nada. Para que todo funcione, debemos correr el siguiente comando:

dart run build_runner build -d
Enter fullscreen mode Exit fullscreen mode

Si estás creando varios modelos, puedes correr un watch para que se creen automáticamente:

dart run build_runner watch -d
Enter fullscreen mode Exit fullscreen mode

features/user/data/data_source

Ya tenemos nuestro modelo, continuemos con nuestro data_source. Este será otro archivo Dart, llamado user_source, que contendrá todas las peticiones a Supabase relacionadas con el usuario:

Estructura de UserSource

Como ves, muchos de nuestros métodos devuelven un UserModel. Para evitar más errores, creamos el modelo antes de seguir con esta parte. Ahora vayamos método por método, agregando nuestra lógica:

Registro de usuario

Para iniciar sesión, primero necesitamos una cuenta. Una opción es hacerlo con un correo electrónico y contraseña. Además, utilizaremos gravatar para generar imágenes de perfil de los usuarios, evitando así gestionar imágenes directamente y evitando posibles problemas legales.

Registro de perfil

Por último, creamos un registro en nuestra tabla profiles, donde se almacenará el perfil público del usuario para enlazarlo con sus partidas y otros usos. Aquí tienes la estructura de la tabla en Supabase:

tabla profiles

Con esto, ya tenemos nuestro método de registro. Ahora podemos iniciar sesión con este otro método:

Inicio de Sesión

Es un método sencillo, donde nuestro factory fromJson entra en acción, transformando la respuesta del SDK a una data class compatible con Dart.

Para no hacer más extenso este artículo, continuaremos con estos métodos en el futuro para completar nuestra feature.

Configuración de Inyección

Como mencionamos antes, en el constructor de la clase indicamos que recibirá un SupabaseClient. Pero, ¿de dónde lo recibirá? Esto lo hacemos con cada elemento de nuestra arquitectura a través del service locator o inyector de dependencias.

class UserSource {
  UserSource(this._client);
  final SupabaseClient _client;
...
Enter fullscreen mode Exit fullscreen mode

Utilizaremos un registerFactory para agregar nuestro UserSource y su dependencia requerida en el constructor.

Inyección de Dependencias

El cual se vería algo así, hasta ahora, donde se ve que le decimos qué dependencia recibe con un tipo en nuestro service locator.

features/user/[data/domain]/repositories

Ahora conectemos nuestras capas data y domain. Para esto, primero creemos nuestro repositorio en la capa domain. Esta será una clase abstracta donde solo declaramos los métodos que utilizaremos en domain, los cuales luego implementaremos en nuestro repositorio de implementación.

Repositorio UserRepository

Aquí está, y si no conoces el paquete dartz, te lo presento. Es una útil herramienta para controlar los estados de error en nuestras diferentes capas. Nuestro método espera dos posibles respuestas: un error o la data correcta, lo cual nos permitirá atrapar estas respuestas y mostrar el estado adecuado en nuestra app.

Para manejar errores, en este caso, crearemos una clase de ayuda llamada Failure, la cual mediante extensiones nos permitirá gestionar diversos errores. Esta la crearemos en core/common/exceptions:

Clase failure

Aquí utilizamos otra librería muy útil llamada Equatable, la cual nos facilita transportar varios campos en nuestra clase y manejar de forma más limpia las comparaciones de clases y valores.

Ahora bien, tenemos en una misma clase dos tipos de error: SupaBaseException y Error, los cuales son extensiones de la misma clase. Nuestros métodos podrían devolver cualquiera de ellos. Además, el campo exception es opcional, ya que no siempre se cumple.

Para finalizar, necesitamos nuestra UserEntity, ya que, como sabemos, en las capas posteriores no deberíamos utilizar los modelos de la capa anterior. Entonces, pasemos a crearla:

UserEntity

Podemos ver que tiene los mismos campos que nuestro modelo, pero más simplificado. A partir de esta capa, no necesitamos realizar más transformaciones ni cambios. Todo es inmutable a partir de este punto.

Regresemos a la capa data en nuestro directorio repositories y creemos nuestro repositorio de implementación user_repo_impl.dart con lo siguiente:

import 'package:dartz/dartz.dart';

import '../../../../core/common/exceptions/failure.dart';
import '../../domain/entities/user.dart';
import '../../domain/repositories/user_repo.dart';
import '../data_source/user_source.dart';

class UserRepoImpl implements UserRepository {
  UserRepoImpl(this._userDataSource);
  final UserSource _userDataSource;

  @override
  Future<Either<Failure, UserEntity?>> getUser() async {
    try {
      final response = await _userDataSource.getUser();
      return Right(response);
    } catch (e) {
      return Left(SupaBaseException(e.toString()));
    }
  }

  @override
  Future<Either<Failure, UserEntity>> signInWithEmailAndPassword(
    String email,
    String password,
  ) async {
    try {
      final response = await _userDataSource.signInWithEmailAndPassword(
        email,
        password,
      );
      return Right(response);
    } catch (e) {
      return Left(SupaBaseException(e.toString()));
    }
  }

  @override
  Future<void> signOut() async {
    await _userDataSource.signOut();
  }

  @override
  Future<Either<Failure, UserEntity>> signUpWithEmailAndPassword(
    String email,
    String password,
    String name,
  ) async {
    try {
      final response = await _userDataSource.signUpWithEmailAndPassword(
        email,
        password,
        name,
      );
      return Right(response);
    } catch (e) {
      return Left(SupaBaseException(e.toString()));
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Con esto, hacemos la conexión entre capas, logrando pasar nuestros datos desde su fuente hasta las capas posteriores. Ahora, toca agregar la inyección de dependencias correspondiente, ya que nuestra implementación espera recibir el data_source llamado UserSource como dependencia. Para eso hacemos lo siguiente:

UserRepo en service locator

Aquí podemos notar algo peculiar: declaramos como tipo principal UserRepository, pero en el factory inicializamos nuestro UserReposImpl, que es el que necesita la dependencia UserSource. Sin embargo, en la siguiente capa domain/use_cases, utilizaremos UserRepository.


Si estás siguiendo el proceso, es posible que te hayas encontrado con el siguiente error:

Image description

Los métodos del repositorio están esperando como respuesta un tipo UserEntity, pero nosotros estamos enviando un UserModel. Este error lo abordaremos en el siguiente artículo, ya que, como pueden ver, hasta ahora hemos hecho bastante trabajo, y este post se ha vuelto mucho más largo en comparación con mis registros anteriores.

Para resolverlo, hay varios caminos, y los analizaremos en la segunda parte de este blog.

Gracias por acompañarme hasta aquí, y espero que haya sido claro. ¡Nos vemos en la siguiente entrega!






Top comments (0)