- PVSM.RU - https://www.pvsm.ru -
Эта статья выросла из публикации “BLoC паттерн на простом примере [1]” где мы разобрались, что это за паттерн и как его применить в классическом простом примере счетчика.
По комментам и для своего лучшего понимания я решил попробовать написать приложение в котором будут получены ответы на вопросы:
Ниже анимашка получившегося примера, а под катом разбор полетов :)
И ещё в конце статьи интересная задачка — как модифицировать приложение для применения Debounce оператора из ReactiveX [2] паттерна (если точнее, то reactiveX — расширение Observer pattern)
Не имеет отношения к BLoC и Provider
Связано с BLoC и Provider
Как следует из определения паттерна BLoC [1] наша задача убрать из виджетов всю логику и работать с данными через класс в котором все входы и выходы — Streams.
При этом, так как класс в котором находится BLoC используется на разных экранах, то нам надо передавать объект созданный из этого класса по всему приложению.
Для этого есть разные методы, а именно:
В данном примере мы будем использовать 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 тут [6] и тут [4].
Далее нам надо сделать так, чтобы при нажатии на кнопочку или свайпе все данные передавались в 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();
}
}
Если мы внимательно посмотрим на этот класс, то увидим, что:
Маленькие задачки для лучшего понимания:
Получаем и передаем данные в приложении
Теперь нам надо передать данные в 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 [7]
Тестировать можно виджеты, можно делать моки, можно e2e.
Мы протестим виджеты и запустим приложение с проверкой как сработало увеличение счетчика. Информация по тестам тут [8] и тут [9].
Тестирование виджетов
Если бы у нас были синхронные данные, то мы могли бы протестить все с помощью виджетов. В нашем же случае мы можем проверить только как виджеты создались и как прошла инициализация.
Код вот тут [7], в коде есть попытки проверить увеличение счетчика после нажатия — выдает ошибку, так как данные идут через 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 [10]
Для запуска теста используем команду
flutter drive --target=test_driver/app.dart
Просто для углубления понимания. В современных приложениях (сайтах) часто используется функция Debounce из ReactiveX.
Например:
Задача: сделать чтобы цифра менялась только если между нажатиями на + или — прошло более 2 секунд. Для этого править только BLoC класс, весь остальной код должен остаться тем же самым.
=========
Вот и все. Если что-то криво или неправильно, поправляйте тут или на github [7], попробуем достичь идеала :)
Всем хорошего кодинга!
Автор: awaik
Источник [11]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/dart/344132
Ссылки в тексте:
[1] BLoC паттерн на простом примере: https://habr.com/ru/post/475404/
[2] Debounce оператора из ReactiveX: http://reactivex.io/documentation/operators/debounce.html
[3] https://pub.dev/packages/shared_preferences: https://pub.dev/packages/shared_preferences
[4] lifting state up: https://flutter.dev/docs/development/data-and-backend/state-mgmt/simple
[5] https://youtu.be/d_m5csmrf7I: https://youtu.be/d_m5csmrf7I
[6] тут: https://pub.dev/packages/provider
[7] https://github.com/awaik/swipe_bloc_flutter: https://github.com/awaik/swipe_bloc_flutter
[8] тут: https://flutter.dev/docs/cookbook/testing
[9] тут: https://habr.com/ru/company/surfstudio/blog/468631/
[10] https://github.com/awaik/swipe_bloc_flutter: https://github.com/awaik/swipe_bloc_flutterhttps://habr.com/ru/company/surfstudio/blog/468631/
[11] Источник: https://habr.com/ru/post/485002/?utm_campaign=485002&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.