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:
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:
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 formatoJSON
(de APIs, SDKs, etc.) a unadata class
compatible conDart
, que se generará automáticamente gracias a la anotación@freezed
.
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
Si estás creando varios modelos, puedes correr un watch
para que se creen automáticamente:
dart run build_runner watch -d
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:
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:
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.
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
:
Con esto, ya tenemos nuestro método de registro. Ahora podemos iniciar sesión con este otro método:
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;
...
Utilizaremos un registerFactory
para agregar nuestro UserSource
y su dependencia requerida en el constructor.
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.
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
:
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:
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()));
}
}
}
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:
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:
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!
RPS Rumble
Proyecto donde aplicaremos la Clean Architecture
con dependency injection
y bloc
. Encuentra todo los articulos en la serie de:
De Cero a Flutter: Mi Viaje para Construir una App y Compartir lo Aprendido
Top comments (0)