Flutter BloC паттерн + Provider + тесты + запоминаем состояние

от автора

Эта статья выросла из публикации “BLoC паттерн на простом примере” где мы разобрались, что это за паттерн и как его применить в классическом простом примере счетчика.

По комментам и для своего лучшего понимания я решил попробовать написать приложение в котором будут получены ответы на вопросы:

  1. Как передавать состояние класса в котором находится BloC по всему приложению
  2. Как написать тесты для этого паттерна
  3. (дополнительный вопрос) Как сохранить состояние данных между запусками приложения оставаясь в рамках BLoC паттерна

Ниже анимашка получившегося примера, а под катом разбор полетов 🙂

И ещё в конце статьи интересная задачка — как модифицировать приложение для применения Debounce оператора из ReactiveX паттерна (если точнее, то reactiveX — расширение Observer pattern)

Описание приложения и базового кода

Не имеет отношения к BLoC и Provider

  1. В приложении есть кнопочки +- и работают свайпы, которые дублируют эти кнопки
  2. Анимация сделана через встроенный во flutter mixin — TickerProviderStateMixin

Связано с BLoC и Provider

  1. Два экрана — на первом свайпаем, на втором отображаются изменения счетчика
  2. Записываем состояние в постоянное хранилище телефона (iOS & Android, пакет https://pub.dev/packages/shared_preferences)
  3. Запись и считывание информации из постоянного хранилища асинхронная, тоже делаем через BLoC

Пишем приложение

Как следует из определения паттерна BLoC наша задача убрать из виджетов всю логику и работать с данными через класс в котором все входы и выходы — Streams.

При этом, так как класс в котором находится BLoC используется на разных экранах, то нам надо передавать объект созданный из этого класса по всему приложению.

Для этого есть разные методы, а именно:

  1. Передача через конструкторы классов, так называемый lifting state up. Не будем использовать, так как очень запутанно получается, потом не отследить передачи состояний.
  2. Сделать из класса где у нас BLoC синглтон и импортировать его где нам нужно. Это просто и удобно, но, с моей сугубо личной точки зрения, усложняет конструктор класса и немного запутывает логику.
  3. Использовать пакет Provider — который рекомендуется командой Flutter для управления состояниями. См. видео https://youtu.be/d_m5csmrf7I

В данном примере мы будем использовать Provider — привести пример всех методов не хватило сил 🙂

Общая структура

Итак, у нас есть класс

class SwipesBloc {     // some stuff }

и, чтобы объект созданный из этого класса, был доступен по всему дереву виджетов, мы, на определенном уровне виджетов приложения, определяем провайдер из этого класса. Я сделал это на самом верху дерева виджетов, но лучше это делать на максимально нужном низком уровне.

class MyApp extends StatelessWidget {   @override   Widget build(BuildContext context) {     return MultiProvider(       providers: [         Provider<SwipesBloc>(create: (_) => SwipesBloc()),       ],       child: MaterialApp(         title: 'Swipe BLoC + Provider',

После добавления этой красивой конструкции в любом виджете внизу дерева нам доступен объект со всеми данными. Подробно как работать с Provider тут и тут.

Далее нам надо сделать так, чтобы при нажатии на кнопочку или свайпе все данные передавались в Stream и, потом, на всех экранах, данные обновлялись из этого же Stream.

Класс для BLoC

Для этого мы создаем класс BLoC, в котором описываем не только потоки, но и получение и запись состояния из постоянного хранилища телефона.

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

Если мы внимательно посмотрим на этот класс, то увидим, что:

  1. Любые свойства доступные снаружи — входы и выходы в Streams.
  2. В конструкторе при первом запуске мы пытаемся получить данные из постоянного хранилища телефона.
  3. Удобно сделана запись в постоянное хранилище телефона

Маленькие задачки для лучшего понимания:

  • Вынести из конструктора кусок кода с .then — красивее сделать отдельный метод.
  • Попробовать реализовать этот класс без провайдера как Singleton

Получаем и передаем данные в приложении

Теперь нам надо передать данные в Stream при нажатии кнопочек или свайпе и получить эти данные на карточке и на отдельном экране.

Есть разные варианты как это сделать, я выбрал классический, мы оборачиваем те части дерева, где нужно получать \ передавать данные в Consumer

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(

Ну и далее получение данных
_swipesBloc.pressedCount,

Передача данных
_swipesBloc.incrementCounter.add(1);

Вот и все, мы получили понятный и расширяемый код в правилах BLoC паттерна.

Рабочий пример https://github.com/awaik/swipe_bloc_flutter

Тесты

Тестировать можно виджеты, можно делать моки, можно e2e.

Мы протестим виджеты и запустим приложение с проверкой как сработало увеличение счетчика. Информация по тестам тут и тут.

Тестирование виджетов

Если бы у нас были синхронные данные, то мы могли бы протестить все с помощью виджетов. В нашем же случае мы можем проверить только как виджеты создались и как прошла инициализация.

Код вот тут, в коде есть попытки проверить увеличение счетчика после нажатия — выдает ошибку, так как данные идут через BLoC.

Для запуска теста используем команду
flutter test

Integration tests (Интеграционные тесты)

В этом варианте теста приложение запускается на эмуляторе, мы можем нажимать кнопочки, свайпать и проверять что получилось в результате.

Для этого мы создаем 2 файла:

test_driver/app.dart
test_driver/app_test.dart

В первом подключаем что нужно, а во втором непосредственно тесты. Для примера я сделал проверки:

  • Начального состояния
  • Увеличения счетчика после нажатия кнопочки

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), ^_^quotquot^_^);       });        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), ^_^quotquot^_^);       });     },   ); }

Код там же https://github.com/awaik/swipe_bloc_flutter

Для запуска теста используем команду
flutter drive --target=test_driver/app.dart

Задача.

Просто для углубления понимания. В современных приложениях (сайтах) часто используется функция Debounce из ReactiveX.

Например:

  1. В строке поиска вводят слово и подсказка вываливается только когда зазор между набором букв более 2 секунд
  2. Когда ставятся лайки, то можно щелкать 10 раз в секунду — запись в базу произойдет если разрыв в щелканьях был более 2-3 секунд
  3. … и т.п.

Задача: сделать чтобы цифра менялась только если между нажатиями на + или — прошло более 2 секунд. Для этого править только BLoC класс, весь остальной код должен остаться тем же самым.

=========

Вот и все. Если что-то криво или неправильно, поправляйте тут или на github, попробуем достичь идеала 🙂

Всем хорошего кодинга!

ссылка на оригинал статьи https://habr.com/ru/post/485002/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *