Este artigo surgiu da publicação “ Padrão BLoC com um exemplo simples ”, onde descobrimos o que é esse padrão e como aplicá-lo em um contra-exemplo clássico simples.
De acordo com os comentários e para meu melhor entendimento, decidi tentar escrever um aplicativo no qual as respostas às perguntas fossem recebidas:
- Como transferir o estado da classe em que o BloC está localizado em todo o aplicativo
- Como escrever testes para esse padrão
- (pergunta adicional) Como manter o estado dos dados entre os lançamentos de aplicativos enquanto permanece no padrão BLoC
Abaixo está uma animashka do exemplo resultante e, sob o corte, há um resumo :)
E no final do artigo, um problema interessante é como modificar o aplicativo para aplicar o operador Debounce a partir do padrão ReactiveX (mais precisamente, o reactiveX é uma extensão do padrão Observer)

Descrição do aplicativo e código base
Não relacionado ao BLoC e ao provedor
- O aplicativo possui botões + - e deslize o dedo que duplicam esses botões.
- Animação feita via flutter mixin embutida - TickerProviderStateMixin
Vinculado ao BLoC e ao provedor
- Duas telas - na primeira deslizamos, na segunda as alterações do contador são exibidas
- Escrevemos o estado no armazenamento permanente do telefone (iOS e Android, pacote https://pub.dev/packages/shared_preferences )
- A gravação e a leitura de informações do armazenamento persistente são assíncronas, e também através do BLoC
Estamos escrevendo um aplicativo
Como segue da definição do padrão BLoC, nossa tarefa é remover toda a lógica dos widgets e trabalhar com dados por meio de uma classe na qual todas as entradas e saídas são Streams.
Ao mesmo tempo, como a classe em que o BLoC está localizado é usada em telas diferentes, precisamos transferir o objeto criado a partir dessa classe por todo o aplicativo.
Existem métodos diferentes para isso, a saber:
- Passando pelos construtores de classe, o chamado estado de elevação se eleva . Nós não vamos usá-lo, pois se mostra muito confuso, então não rastreie as transferências de estado.
- Faça da classe onde temos o BLoC singleton e importe-o onde precisarmos. É simples e conveniente, mas, do meu ponto de vista puramente pessoal, complica o construtor da classe e confunde um pouco a lógica.
- Use o pacote Provider - recomendado pela equipe do Flutter para o gerenciamento de estado. Veja o vídeo
Neste exemplo, usaremos o Provedor - para dar um exemplo de todos os métodos que não tinham força suficiente :)
Estrutura geral
Então nós temos uma aula
class SwipesBloc { // some stuff }
e para que um objeto criado a partir desta classe seja acessível em toda a árvore de widgets, nós, em um certo nível de widgets de aplicativo, definimos um provedor dessa classe. Fiz isso no topo da árvore de widgets, mas é melhor fazê-lo no nível mais baixo possível.
class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MultiProvider( providers: [ Provider<SwipesBloc>(create: (_) => SwipesBloc()), ], child: MaterialApp( title: 'Swipe BLoC + Provider',
Depois de adicionar esse belo design a qualquer widget na parte inferior da árvore, um objeto com todos os dados está disponível para nós. Detalhes sobre como trabalhar com o Provedor aqui e aqui .
Em seguida, precisamos garantir que quando você clica no botão ou desliza todos os dados são transferidos para o Stream e, em todas as telas, os dados são atualizados a partir do mesmo Stream.
Classe para BLoC
Para fazer isso, criamos uma classe BLoC na qual descrevemos não apenas os fluxos, mas também o recebimento e a gravação do status do armazenamento permanente do telefone.
import 'dart:async'; import 'package:rxdart/rxdart.dart'; import 'package:shared_preferences/shared_preferences.dart'; class SwipesBloc { Future<SharedPreferences> prefs = SharedPreferences.getInstance(); int _counter; SwipesBloc() { prefs.then((val) { if (val.get('count') != null) { _counter = val.getInt('count') ?? 1; } else { _counter = 1; } _actionController.stream.listen(_changeStream); _addValue.add(_counter); }); } final _counterStream = BehaviorSubject<int>.seeded(1); Stream get pressedCount => _counterStream.stream; void get resetCount => _actionController.sink.add(null); Sink get _addValue => _counterStream.sink; StreamController _actionController = StreamController(); StreamSink get incrementCounter => _actionController.sink; void _changeStream(data) async { if (data == null) { _counter = 1; } else { _counter = _counter + data; } _addValue.add(_counter); prefs.then((val) { val.setInt('count', _counter); }); } void dispose() { _counterStream.close(); _actionController.close(); } }
Se olharmos cuidadosamente para essa classe, veremos que:
- Quaisquer propriedades disponíveis externamente são as entradas e saídas do Streams.
- Na primeira execução no designer, tentamos obter dados do armazenamento permanente do telefone.
- Convenientemente gravado no armazenamento permanente do telefone
Pequenas tarefas para uma melhor compreensão:
- Remover um pedaço de código de .then do construtor é mais bonito para criar um método separado.
- Tente implementar esta classe sem um provedor como Singleton
Receba e transmita dados no aplicativo
Agora precisamos transferir dados para o Stream ao clicar em botões ou deslizar o dedo e colocar esses dados no cartão e em uma tela separada.
Existem diferentes opções de como fazer isso. Eu escolhi o clássico, envolvemos as partes da árvore nas quais você precisa receber / transferir dados para o Consumidor.
return Scaffold( body: Consumer<SwipesBloc>( builder: (context, _swipesBloc, child) { return StreamBuilder<int>( stream: _swipesBloc.pressedCount, builder: (context, snapshot) { String counterValue = snapshot.data.toString(); return Stack( children: <Widget>[ Container(
Bem, então obtendo dados
_swipesBloc.pressedCount,
Transferência de dados
_swipesBloc.incrementCounter.add(1);
É tudo, temos um código claro e extensível nas regras do padrão BLoC.
Exemplo de trabalho
Testes
Você pode testar widgets, criar mokas, e2e.
Testaremos os widgets e executaremos o aplicativo com uma verificação de como o aumento de contador funcionou. Informações sobre testes aqui e aqui .
Teste de widget
Se tivéssemos dados síncronos, poderíamos testar tudo com widgets. No nosso caso, podemos apenas verificar como os widgets foram criados e como foi a inicialização.
O código está aqui , no código há tentativas de verificar o aumento do contador após clicar - isso gera um erro, pois os dados passam pelo BLoC.
Para executar o teste, use o comando
flutter test
Testes de integração
Nesta opção de teste, o aplicativo é executado no emulador, podemos pressionar botões, deslizar e verificar o que aconteceu como resultado.
Para isso, criamos 2 arquivos:
test_driver / app.dart
test_driver / app_test.dart
No primeiro, conectamos o que é necessário e, no segundo, testamos diretamente. Por exemplo, eu fiz as verificações:
- Estado inicial
- Contador de incrementos após pressionar um botão
import 'package:flutter_driver/flutter_driver.dart'; import 'package:test/test.dart'; void main() { group( 'park-flutter app', () { final counterTextFinder = find.byValueKey('counterKey'); final buttonFinder = find.byValueKey('incrementPlusButton'); FlutterDriver driver; setUpAll(() async { driver = await FlutterDriver.connect(); }); tearDownAll(() async { if (driver != null) { driver.close(); } }); test('test init value', () async { expect(await driver.getText(counterTextFinder), ^_^quot quot^_^); }); test('test + 1 value after tapped', () async { await driver.tap(buttonFinder); // Then, verify the counter text is incremented by 1. expect(await driver.getText(counterTextFinder), ^_^quot quot^_^); }); }, ); }
Código lá
Para executar o teste, use o comando
flutter drive --target=test_driver/app.dart
Desafio
Apenas para aprofundar sua compreensão. Em aplicativos modernos (sites), a função Debounce do ReactiveX é frequentemente usada.
Por exemplo:
- Uma palavra é inserida na barra de pesquisa e uma dica cai somente quando o intervalo entre o conjunto de letras é superior a 2 segundos
- Quando as curtidas são colocadas, você pode clicar 10 vezes por segundo - a gravação no banco de dados ocorrerá se a diferença nos cliques for superior a 2-3 segundos
- ... etc.
Tarefa: alterar o dígito apenas se decorrerem mais de 2 segundos entre as prensas de + ou -. Para fazer isso, edite apenas a classe BLoC, o restante do código deve permanecer o mesmo.
Isso é tudo. Se algo estiver torto ou errado, corrija aqui ou no github , tente alcançar o ideal :)
Boa codificação para todos!