Pular para o conteúdo

Arquitetura

Arquitetura Bloc

Usar a biblioteca bloc nos permite separar nossa aplicação em três camadas:

  • Apresentação
  • Lógica de Negócios
  • Dados
    • Repositório
    • Provedor de Dados

Começaremos na camada de nível mais baixo (mais distante da interface do usuário) e avançaremos até a camada de apresentação.

Camada de Dados

A responsabilidade da camada de dados é recuperar/manipular dados de uma ou mais fontes.

A camada de dados pode ser dividida em duas partes:

  • Repositório
  • Provedor de Dados

Esta camada é o nível mais baixo da aplicação e interage com bancos de dados, requisições de rede e outras fontes de dados assíncronas.

Provedor de Dados

A responsabilidade do provedor de dados é fornecer dados brutos. O provedor de dados deve ser genérico e versátil.

O provedor de dados geralmente expõe APIs simples para executar operações CRUD. Podemos ter os métodos createData, readData, updateData, e deleteData como parte da nossa camada de dados.

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

Repositório

A camada de repositório é um wrapper em torno de um ou mais provedores de dados com os quais a camada Bloc se comunica.

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 pode ver, nossa camada de repositório pode interagir com vários provedores de dados e executar transformações nos dados antes de entregar o resultado para a camada de lógica de negócios.

Camada de Lógica de Negócios

A responsabilidade da camada de lógica de negócios é responder às entradas da camada de apresentação com novos estados. Esta camada pode depender de um ou mais repositórios para recuperar os dados necessários para construir o estado do aplicativo.

Pense na camada de lógica de negócios como a ponte entre a interface do usuário (camada de apresentação) e a camada de dados. A camada de lógica de negócios é notificada por eventos/ações da camada de apresentação e então se comunica com o repositório para criar um novo estado para a camada de apresentação consumir.

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;
}

Comunicação Bloc-to-Bloc

Como os blocs expõem streams, pode ser tentador criar um bloc que ouça outro bloc. Você não deve fazer isso. Existem alternativas melhores do que recorrer ao código abaixo:

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();
}
}

Embora o código acima esteja livre de erros (e até mesmo se limpe depois de executado), ele tem um grande problema: ele cria uma dependência entre dois blocs.

Em geral, dependências entre duas entidades na mesma camada arquitetural devem ser evitadas a todo custo, pois criam um alto acoplamento que é difícil de manter. Como os blocs ficam na camada arquitetural da lógica de negócios, nenhum bloc deve saber sobre qualquer outro bloc.

Camadas de Arquitetura da Aplicação

Um bloc só deve receber informações por meio de eventos e de repositórios injetados (ou seja, repositórios fornecidos ao bloc em seu construtor).

Se você estiver em uma situação em que um bloc precisa responder a outro bloc, você tem duas outras opções. Você pode enviar o problema para uma camada acima (a camada de apresentação) ou para uma camada abaixo (a camada de domínio).

Conectando Blocs com Apresentação

Você pode usar um BlocListener para escutar um bloc e adicionar um evento a outro bloc sempre que o primeiro bloc for alterado.

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());
},
),
);
}
}

O código acima impede que o SecondBloc precise saber algo sobre o FirstBloc, encorajando o baixo acoplamento. O aplicativo flutter_weather usa essa técnica para alterar o tema do aplicativo com base nas informações meteorológicas recebidas.

Em algumas situações, você pode não querer acoplar dois blocs na camada de apresentação. Em vez disso, pode fazer sentido que dois blocs compartilhem a mesma fonte de dados e atualizem sempre que os dados mudarem.

Conectando Blocs com Domínio

Dois blocs podem escutar um stream de um repositório e atualizar seus estados, independente um do outro, sempre que os dados do repositório mudarem. Usar repositórios reativos para manter o estado sincronizado é comum em aplicativos empresariais de larga escala.

Primeiro, crie ou use um repositório que forneça um Stream de dados. Por exemplo, o seguinte repositório expõe um stream interminável das mesmas ideias de aplicativos:

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

O mesmo repositório pode ser injetado em cada bloc que precisa reagir a novas ideias de aplicativo. Abaixo está um AppIdeaRankingBloc que produz um estado para cada ideia de aplicativo recebida do repositório acima:

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 saber mais sobre como usar streams com Bloc, consulte Como usar o Bloc com streams e concorrência.

Camada de Apresentação

A responsabilidade da camada de apresentação é resolver como se renderizar com base em um ou mais estados do bloc. Além disso, ela deve manipular as entradas do usuário e os eventos do ciclo de vida do aplicativo.

A maioria dos fluxos de aplicativos começa com um evento AppStart que aciona a aplicação para buscar alguns dados para apresentar ao usuário.

Nesse cenário, a camada de apresentação adicionaria um evento AppStart.

Além disso, a camada de apresentação terá que descobrir o que renderizar na tela com base no estado da camada do bloc.

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

Até agora, embora tenhamos alguns trechos de código, tudo isso tem sido muito de alto nível. Na seção tutorial, vamos juntar tudo isso enquanto construímos vários aplicativos de exemplo diferentes.