Padrão BloC Flutter + Provedor + testes + lembre-se do estado

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:


  1. Como transferir o estado da classe em que o BloC está localizado em todo o aplicativo
  2. Como escrever testes para esse padrão
  3. (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


  1. O aplicativo possui botões + - e deslize o dedo que duplicam esses botões.
  2. Animação feita via flutter mixin embutida - TickerProviderStateMixin

Vinculado ao BLoC e ao provedor


  1. Duas telas - na primeira deslizamos, na segunda as alterações do contador são exibidas
  2. Escrevemos o estado no armazenamento permanente do telefone (iOS e Android, pacote https://pub.dev/packages/shared_preferences )
  3. 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:


  1. 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.
  2. 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.
  3. 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:


  1. Quaisquer propriedades disponíveis externamente são as entradas e saídas do Streams.
  2. Na primeira execução no designer, tentamos obter dados do armazenamento permanente do telefone.
  3. 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:


  1. Uma palavra é inserida na barra de pesquisa e uma dica cai somente quando o intervalo entre o conjunto de letras é superior a 2 segundos
  2. 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
  3. ... 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!

Source: https://habr.com/ru/post/pt485002/


All Articles