Saltearse al contenido

Arquitectura

Arquitectura Bloc

Usar la biblioteca bloc nos permite separar nuestra aplicación en tres capas:

  • Presentación
  • Lógica de Negocio
  • Datos
    • Repositorio
    • Proveedor de Datos

Vamos a comenzar en la capa de nivel más bajo (más alejada de la interfaz de usuario) y trabajaremos hacia arriba hasta la capa de presentación.

Capa de Datos

La responsabilidad de la capa de datos es recuperar/manipular datos de una o más fuentes.

La capa de datos se puede dividir en dos partes:

  • Repositorio
  • Proveedor de Datos

Esta capa es el nivel más bajo de la aplicación e interactúa con bases de datos, solicitudes de red y otras fuentes de datos asíncronas.

Proveedor de Datos

La responsabilidad del proveedor de datos es proporcionar datos en bruto. El proveedor de datos debe ser genérico y versátil.

El proveedor de datos generalmente expondrá APIs simples para realizar operaciones CRUD. Podríamos tener un método createData, readData, updateData y deleteData como parte de nuestra capa de datos.

data_provider.dart
class DataProvider {
Future<RawData> readData() async {
// Read from DB or make network request etc...
}
}

Repositorio

La capa de repositorio es un envoltorio alrededor de uno o más proveedores de datos con los que se comunica la capa Bloc.

repository.dart
class Repository {
final DataProviderA dataProviderA;
final DataProviderB dataProviderB;
Future<Data> getAllDataThatMeetsRequirements() async {
final RawDataA dataSetA = await dataProviderA.readData();
final RawDataB dataSetB = await dataProviderB.readData();
final Data filteredData = _filterData(dataSetA, dataSetB);
return filteredData;
}
}

Como puedes ver, nuestra capa de repositorio puede interactuar con múltiples proveedores de datos y realizar transformaciones en los datos antes de entregar el resultado a la capa de lógica de negocio.

Capa de Lógica de Negocio

La responsabilidad de la capa de lógica de negocio es responder a la entrada de la capa de presentación con nuevos estados. Esta capa puede depender de uno o más repositorios para recuperar los datos necesarios para construir el estado de la aplicación.

Piensa en la capa de lógica de negocio como el puente entre la interfaz de usuario (capa de presentación) y la capa de datos. La capa de lógica de negocio es notificada de eventos/acciones desde la capa de presentación y luego se comunica con el repositorio para construir un nuevo estado para que la capa de presentación lo consuma.

business_logic_component.dart
class BusinessLogicComponent extends Bloc<MyEvent, MyState> {
BusinessLogicComponent(this.repository) {
on<AppStarted>((event, emit) {
try {
final data = await repository.getAllDataThatMeetsRequirements();
emit(Success(data));
} catch (error) {
emit(Failure(error));
}
});
}
final Repository repository;
}

Comunicación Bloc a Bloc

Debido a que los blocs exponen streams, puede ser tentador hacer un bloc que escuche a otro bloc. No deberías hacer esto. Hay mejores alternativas que recurrir al siguiente código:

tightly_coupled_bloc.dart
class TightlyCoupledBloc extends Bloc {
final OtherBloc otherBloc;
late final StreamSubscription otherBlocSubscription;
TightlyCoupledBloc(this.otherBloc) {
// No matter how much you are tempted to do this, you should not do this!
// Keep reading for better alternatives!
otherBlocSubscription = otherBloc.stream.listen((state) {
add(MyEvent());
});
}
@override
Future<void> close() {
otherBlocSubscription.cancel();
return super.close();
}
}

Aunque el código anterior no tiene errores (e incluso se limpia después de sí mismo), tiene un problema mayor: crea una dependencia entre dos blocs.

Generalmente, las dependencias entre dos entidades en la misma capa arquitectónica deben evitarse a toda costa, ya que crea un acoplamiento fuerte que es difícil de mantener. Dado que los blocs residen en la capa arquitectónica de lógica de negocio, ningún bloc debería conocer a otro bloc.

Capas de Arquitectura de la Aplicación

Un bloc solo debería recibir información a través de eventos y de repositorios inyectados (es decir, repositorios dados al bloc en su constructor).

Si te encuentras en una situación donde un bloc necesita responder a otro bloc, tienes dos opciones. Puedes empujar el problema hacia arriba (a la capa de presentación) o hacia abajo (a la capa de dominio).

Conectando Blocs a través de la Presentación

Puedes usar un BlocListener para escuchar a un bloc y agregar un evento a otro bloc cada vez que el primer bloc cambie.

my_widget.dart
class MyWidget extends StatelessWidget {
const MyWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BlocListener<FirstBloc, FirstState>(
listener: (context, state) {
// When the first bloc's state changes, this will be called.
//
// Now we can add an event to the second bloc without it having
// to know about the first bloc.
context.read<SecondBloc>().add(SecondEvent());
},
child: TextButton(
child: const Text('Hello'),
onPressed: () {
context.read<FirstBloc>().add(FirstEvent());
},
),
);
}
}

El código anterior evita que SecondBloc necesite conocer a FirstBloc, fomentando el acoplamiento débil. La aplicación flutter_weather usa esta técnica para cambiar el tema de la aplicación basado en la información del clima que se recibe.

En algunas situaciones, puede que no quieras acoplar dos blocs en la capa de presentación. En su lugar, a menudo tiene sentido que dos blocs compartan la misma fuente de datos y se actualicen cada vez que los datos cambien.

Conectando Blocs a través del Dominio

Dos blocs pueden escuchar un stream de un repositorio y actualizar sus estados independientemente cada vez que los datos del repositorio cambien. Usar repositorios reactivos para mantener el estado sincronizado es común en aplicaciones empresariales a gran escala.

Primero, crea o usa un repositorio que proporcione un Stream de datos. Por ejemplo, el siguiente repositorio expone un stream interminable de las mismas pocas ideas de aplicaciones:

app_ideas_repository.dart
class AppIdeasRepository {
int _currentAppIdea = 0;
final List<String> _ideas = [
"Future prediction app that rewards you if you predict the next day's news",
'Dating app for fish that lets your aquarium occupants find true love',
'Social media app that pays you when your data is sold',
'JavaScript framework gambling app that lets you bet on the next big thing',
'Solitaire app that freezes before you can win',
];
Stream<String> productIdeas() async* {
while (true) {
yield _ideas[_currentAppIdea++ % _ideas.length];
await Future<void>.delayed(const Duration(minutes: 1));
}
}
}

El mismo repositorio puede ser inyectado en cada bloc que necesite reaccionar a nuevas ideas de aplicaciones. A continuación se muestra un AppIdeaRankingBloc que emite un estado por cada idea de aplicación entrante del repositorio anterior:

app_idea_ranking_bloc.dart
class AppIdeaRankingBloc
extends Bloc<AppIdeaRankingEvent, AppIdeaRankingState> {
AppIdeaRankingBloc({required AppIdeasRepository appIdeasRepo})
: _appIdeasRepo = appIdeasRepo,
super(AppIdeaInitialRankingState()) {
on<AppIdeaStartRankingEvent>((event, emit) async {
// When we are told to start ranking app ideas, we will listen to the
// stream of app ideas and emit a state for each one.
await emit.forEach(
_appIdeasRepo.productIdeas(),
onData: (String idea) => AppIdeaRankingIdeaState(idea: idea),
);
});
}
final AppIdeasRepository _appIdeasRepo;
}

Para más información sobre cómo usar streams con Bloc, consulta Cómo usar Bloc con streams y concurrencia.

Capa de Presentación

La responsabilidad de la capa de presentación es determinar cómo renderizarse a sí misma en función de uno o más estados del bloc. Además, debe manejar la entrada del usuario y los eventos del ciclo de vida de la aplicación.

La mayoría de los flujos de aplicaciones comenzarán con un evento AppStart que desencadena la aplicación para obtener algunos datos para presentar al usuario.

En este escenario, la capa de presentación agregaría un evento AppStart.

Además, la capa de presentación tendrá que determinar qué renderizar en la pantalla en función del estado de la capa de bloc.

presentation_component.dart
class PresentationComponent {
PresentationComponent({required this.bloc}) {
bloc.add(AppStarted());
}
final Bloc bloc;
build() {
// render UI based on bloc state
}
}

Hasta ahora, aunque hemos tenido algunos fragmentos de código, todo esto ha sido bastante a alto nivel. En la sección de tutoriales vamos a juntar todo esto mientras construimos varias aplicaciones de ejemplo diferentes.