Вчера мой хороший товарищ сказал что-то типа "я же пишу простяцкое оффлайн приложение, мне ни к чему все эти стримы и потоки". Я даже растерялся, а потом подумал, что это заблуждение могут разделять и другие кодеры.
Ниже буквально в 50 строк я на известном примере покажу, что реактивность
а) это не про оффлайн/онлайн
б) это очень просто
в) очень хороша для упрощения практически любого кода
Моим поспешным критикам,
которые без оглядки бросаются в бой, считая что BlocProvider
— это provider, рекомендую для общего развития почитать сперва базовую статью, ссылка на которую есть на странице либы flutter_bloc, в первой же строке описания.
Всем известный пример "Счетчик", который генерится при создании Flutter проекта, является мальчиком для битья хорошей стартовой точкой для демонстрации множества практик.
Итак, он содержит MyHomePage extends StatefulWidget
, метод _incrementCounter
для команды инкремента и setState
для перерисовки всей иерархии виджетов.
Внесем реактивность при помощи библиотеки rxdart
и нескольких несложных шагов:
Добавим библиотеку в pubspec.yaml
dependencies:
...
rxdart: 0.22.2
Изменим архитектуру счетчика и добавим event
class _Counter {
int _count;
int get count => _count;
_Counter(this._count)
: this.onCounterUpd = BehaviorSubject<int>.seeded(_count);
/// Создадим евент.
final BehaviorSubject<int> onCounterUpd;
/// Вынесем инкремент за пределы виджета, добавим генерацию события.
Future incrementCounter() async {
onCounterUpd.add(++_count);
}
}
final _counter = _Counter(5);
Сделаем класс StatelessWidget
/// Сделаем класс "без состояния"
class MyHomeRxPage extends StatelessWidget {
final title;
/// ! - Можно сделать константным классом
const MyHomeRxPage({Key key, this.title}) : super(key: key);
...
Обернем виджет отображения в StreamBuilder и изменим вызов метода инкремента
StreamBuilder<int>(
stream: _counter.onCounterUpd,
builder: (context, snapshot) {
return Text(
'${snapshot.data}',
style: Theme.of(context).textTheme.display1,
);
}),
...
floatingActionButton: FloatingActionButton(
onPressed: _counter.incrementCounter,
...
Вот и все. Полностью это выглядит так
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:rxdart/rxdart.dart';
class _Counter {
int _count;
int get count => _count;
_Counter(this._count)
: this.onCounterUpd = BehaviorSubject<int>.seeded(_count);
/// Создадим евент.
final BehaviorSubject<int> onCounterUpd;
/// Вынесем инкремент за пределы виджета, добавим генерацию события.
Future incrementCounter() async {
onCounterUpd.add(++_count);
}
}
final _counter = _Counter(5);
///
class MyHomeRxPage extends StatelessWidget {
final title;
/// ! - Можно сделать константным классом
const MyHomeRxPage({Key key, this.title}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Invoke "debug painting" (press "p" in the console, choose the
// "Toggle Debug Paint" action from the Flutter Inspector in Android
// Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
// to see the wireframe for each widget.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
StreamBuilder<int>(
stream: _counter.onCounterUpd,
builder: (context, snapshot) {
return Text(
'You have pushed the button ${snapshot.data} times:',
);
}),
// Text(
// 'You have pushed the button this many times:',
// ),
/// 6.
StreamBuilder<int>(
stream: _counter.onCounterUpd,
builder: (context, snapshot) {
return Text(
'${snapshot.data}',
style: Theme.of(context).textTheme.display1,
);
}),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _counter.incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
Теперь код реактивен, лаконичен, избавлен от лишних перерисовок, и легко расширяем.
Например, если в момент изменения счетчика понадобится менять текст другого виджета, достаточно сделать так:
StreamBuilder<int>(
stream: onCounterUpd,
builder: (context, snapshot) {
return Text(
'You have pushed the button ${snapshot.data} times:',
);
}),
// Text(
// 'You have pushed the button this many times:',
// ),
и вуаля!
Для сравнения попробуйте сделать это же с InheritedWidget, или другим паттерном.
Итак, надеюсь, я показал, что
- Реактивность это очень несложно. Гораздо проще
InheritedWidgets
,BlocProvider
, etc. - Реактивность не про оффлайн/онлайн. Она про архитектуру. Как я показал, в самых простых случаях даже не нужно вносить дополнительные классы, чтобы ее применять.
- Реактивность это отзывчивые UI, быстрое расширение функционала, изящное разделение кода на слои любого типа: MVC, MVP, MVI, MVVM, все, что пожелается.
Код примера (ветка iter_0004_rxdart
)
Отредактировано часом позже
Зря сделал слишком просто, получил щелбанов за "глобальные переменные" и неправильное понимание инициализации BehaviorSubject
, исправился
Комментарии (30)
epishman
08.11.2019 08:16Не знаю, для меня как-то естественней смотрится Provider, там по крайней мере понятно где хранить стейт — прямо в дереве виджетов. А эта реактивщина заставляет выносить стейт куда-то вовне, и дискутировать по этому поводу.
rookie_cruekie Автор
08.11.2019 08:29Дело в том, что
реактивщина
не только про стейты. Ведь она позволяет обмениваться событиями между любыми объектами, и невизуальными в бизнес-слое, и все это в едином стиле. Кроме того, мощьreactive-way
проявляется и в потоковой обработке данных, используя операторы. Внося реактивность в проект даже на стадии обмена сообщениями, можно впоследствии постепенно начинать применять ее все шире.epishman
08.11.2019 09:01Я согласен. Но в простых приложениях нет асинхронных потоков, а считать клики от кнопки потоком, это немного притянуто...
rookie_cruekie Автор
08.11.2019 14:55+1Ну, во-первых, насколько часто нам приходится писать счетчики? Начиная даже самый простой проект, потихоньку приходишь к усложнению. А вот представьте, Вам потребовалось вставить обратный отсчет для блокировки кнопки между нажатиями. Обойдетесь без фонового потока? А если нет, как будете реализовывать?
Позвольте представить решение в рамках концепции данной статьи:
/// делаем задержку в 3 секунды, с уведомлением через каждую секунду. class _Counter { int _count; /// Счетчик обратного отсчета int _countdown = 0; int get count => _count; _Counter(this._count) : this.onCounterUpd = BehaviorSubject<int>.seeded(_count), this.onCountdownUpd = BehaviorSubject<int>.seeded(0); final BehaviorSubject<int> onCounterUpd; /// Евент обратного отсчета final BehaviorSubject<int> onCountdownUpd; /// Вынесем инкремент за пределы виджета, добавим генерацию события. Future incrementCounter() async { if(_countdown <= 0) { onCounterUpd.add(++_count); /// Запуск таймера, с вочдогом и генерацией евентов. _countdown = 3; onCountdownUpd.add(_countdown); Observable .periodic(Duration(seconds: 1), (_) => --_countdown) .take(3) .listen((e) => onCountdownUpd.add(_countdown)); } } }
после чего делаем FAB реактивным,
/// Кнопка стала реактивной floatingActionButton: StreamBuilder<int>( initialData: _counter.onCountdownUpd.value, stream: _counter.onCountdownUpd, builder: (context, snapshot) { return FloatingActionButton( onPressed: snapshot.data <= 0 ? _counter.incrementCounter : null, tooltip: 'Increment', backgroundColor: snapshot.data <= 0 ? Theme.of(context).primaryColor : Colors.grey, child: Icon(Icons.add), ); } ), // This trailing comma makes auto-formatting nicer for build methods.
и добавляем рекативное же упоминание
/// Реактивная надпись StreamBuilder<int>( stream: _counter.onCountdownUpd, builder: (context, snapshot) { return Text( 'Rest ${snapshot.data} seconds', style: Theme.of(context).textTheme.title, ); }),
epishman
08.11.2019 23:47Спасибо, убедили, реактивщина действительно правильный подход. Я просто на днях внезапно разлюбил флаттер, уж слишком код получается многословным и нечитаемым, особенно верстка, похоже вернусь назад на реакт и веб-компоненты, там хоть понятно все, а тут каждый квартал новые классы появляются, новые подходы. Вроде и работу по нему найти реально, но что-то неэстетичное есть в этом языке и фреймворке, уже сколько раз — загораюсь, начинаю что-то писать, и потом бросаю под тяжестью новых ненужных знаний :)
cutzmf
08.11.2019 08:41+1Автор непонимает основ Rx и Flutter
Ошибки
1. инициализация значения в BehaviorSubject не через named конструктор .seeded(initValue)
2. эта ошибка вытекла из первой, но показала непонимание основ Flutter.
Нельзя ничего инициировать в методе build() любого типа виджетаrookie_cruekie Автор
08.11.2019 08:43Да, согласен, косяк. Даже два.
- Поправимо
- Не отменяет самого принципа и эффективности применения
cutzmf
08.11.2019 09:13по пункту 2
api.flutter.dev/flutter/widgets/StatelessWidget-class.html
The build method of a stateless widget is typically only called in three situations: the first time the widget is inserted in the tree, when the widget's parent changes its configuration, and when an InheritedWidget it depends on changes.
api.flutter.dev/flutter/widgets/StatelessWidget/build.html
The implementation of this method must only depend on:
the fields of the widget, which themselves must not change over time
Не зная первой цитаты ты стреляешь себе в ногу в будущем(ближайшем)
Не зная второй цитаты ты опираешься на внешние данные, полностью перечя ей(upd тут я неправ)
Ты не читал документацию или не перечитал с пониманиемandrew8712
08.11.2019 09:53Может, обороты-то сбавите, не? Высокомерие так и хлещет.
Автор статьи пишет, старается. Да, не все 100% идеально, а вы тут сразу тыкать начали, как вахтер какой-тоcutzmf
08.11.2019 10:01Вы пришли из телеги, сэр, здесь обороты нормальные,
ошибки обозначены и описаны, показано что надо изменить.
Не нагнетайте
cutzmf
08.11.2019 09:24+1поправь
final onCounterUpd = BehaviorSubject.seeded(count)
убери из конструктора
_Counter(this.count) {
onCounterUpd.add(count);
}
убери из StreamBuilder'а
StreamBuilder(
initialData: _counter.onCounterUpd.value,
stream: _counter.onCounterUpd,
pretorean
08.11.2019 09:42+1Как демонстрация идеи хорошо.
Реквестирую следующую статью, где будет проиллюстрирована эта идея на примере какой то популярной архитектуры.rookie_cruekie Автор
08.11.2019 09:58+1в процессе. Есть прикольная архитектура MVI, я ее применяю и в процессе слегка модернизирую. Вот спустя месяцок накидаю статью с реальным примером.
cutzmf
08.11.2019 10:06habr.com/ru/post/448776
Вы в статье против BLoС, но вы не в курсе что flutter_bloc — это дериватив MVU(родоначальник подхода ELM TEA)
guide.elm-lang.org/architecture
In fact, projects like Redux have been inspired by The Elm Architecture, so you may have already seen derivatives of this pattern
MVI это то же дериватив MVU и бойлерплейта там тоже много
flutter_bloc, MVU(TEA), Redux, MVI эквивалентны (просто разные реализации)
Получается что пчёлы против мёда и ваши статьи противоречят вашему же коментарию вышеrookie_cruekie Автор
08.11.2019 10:17Да я в курсе и нет, я не против. Каждый выбирает для себя. Я за стиль, простоту, изящество решений, по мне так этому всему отвечает именно ReactiveX. Он закрывает для меня почти все вопросы по взаимодействию и с View и между всеми слоями архитектуры с минимумом кода и максимумом выразительности. Это я еще не беру в расчет мощь его цепочек операторов.
Я честно наваял часть проекта на MVU, но потом убился о количество кода, которое надо добавлять. Да, можно было написать генератор, но код-то никуда бы ни делся и болтался, мозоля глаза, да и хороший генератор писать тоже надо дофига времени на отладку.
MVI я не использую в чистом виде по этой же причине, но мне импонирует immutable-way, и передача упакованного состояния в одном флаконе. Позже я черкану статью о своей реализации.
cutzmf
08.11.2019 10:24flutter_bloc (дальше упоминаю как BLoC) Felix'a Angelo это чуть по-другому реализованный MVI от Dorfman'а
UDF + Rx
Как тебе угодил MVI, но не угодил BLoC?
Intent -> Model -> View
Event -> State -> View
эквивалентные вещи и через тот же Rxrookie_cruekie Автор
08.11.2019 10:42Приходится повторяться специально для Вас, что мне не нравится раздутость кода, в которое обернуты оба решения, и я не применяю MVI в том виде, в котором он есть. Вы не вчитываетесь, а видимо, просто еще на утреннем автовзводе от общения в
telegram
.
Я бы с удовольствием увидел Ваши решения счетчика на BLoC, MVU, и сравнить количество строк, при одинаковом результате. Попробуйте принять, как данность, что это статься просто про другой подход к решению задач программирования. Пожалуйста. Спасибо.cutzmf
08.11.2019 10:54меньшее количество строк != читабельность && easy поддержка
BLoC & MVI смысл один один поток на вход, один на выходе
+ предсказуемый стейт
Вы пишете свой велосипед, который может потерять этот +, если не один единственный поток на вход(очередь событий)
Почитайте про синдром NIH
Жду статью про велосипед дериватив MVU(TEA), Redux, MVI, BLoC
cutzmf
08.11.2019 10:58Я не пишу решения счетчика на BLoC, оно уже есть от оригинала(Felix) в документации.
Я вношу вклад в сообщество помогая в чатике и критикуя явные проблемы, особенно если это публичная статья для новичков с грубыми ошибками.
plugfox
08.11.2019 15:06Вы не правы.
В своей статье Вы сравниваете архитектуру (стейт менеджмент), виджет (ui элемент), di и все это с оберткой над dart:async. Прям целая каша в голове.
Никто не сомневается, что можно написать hello world вообще не оперируя такими понятиями и вообще используя только setState и Future builder.
Собственно, в этом и состоит к вам претензия.
PS: если хотите, распишу вам подробнее, зачем нужен DI (Provider, оберточка над Inheriting Widget), зачем стейт менеджмент (BLoC, который, между прочим, на rxdart).rookie_cruekie Автор
08.11.2019 15:17Давайте так, спасибо конечно, но мне тут не надо ничего расписывать, и так уже простыня-простынища.
Предлагаю Вам написать статью, в которой Вы представите свой взгляд. свои подходы, и свое видение.
Я достаточно знаю и про DI, и про BLoC, статья не об этом. Статья о том, что можно эффективно и просто достигать целей, используя очень простые решения. Я писал и с использованием Provider, и с использованием MVU, и в конечном итоге остановился на том, что безумное количество дополнительного кода не может служить оправданием того, что я использую хайповые решения. Видите ли, я сам себе режиссер, и мне не надо ходить на собеседование и ожидать тупых вопросов типа "а знаете ли вы вот эти стопицот модных технологий?", а в реальной жизни дауншифтинг на rxdart уменьшил мой код вдвое(!), не затронув производительность.
Вот о чем статья. Для сравнения технологий, практик, паттернов. Так что пишите свою, (и наполучаете от хейтеров в карму :) ).
PS. Да, вдогон. Про DI, Provider я вообще не упоминал, а на КДПВ указан концепт провайдера для BLoC, по аналогии с тем, что в этой статье. Пожалуйста, читайте статьи, которые комментируете, а не додумывайте свое за автора. Спасибо.
cutzmf
08.11.2019 13:58-1habr.com/ru/post/474968/#comment_20862172
минусатор, есть аргументированные возражения этому коменту?
cutzmf
08.11.2019 11:04Cделать приватным final BehaviorSubject onCounterUpd;
Наружу только Observable или Stream, потому что я могу миновать incrementCounter() и сунуть туда какое захочу value и оно минует вашу логику
Это как раз вопрос велосипеда, он уже несостоятелен и его приходится рихтовать.
Зачем здесь async функция?FutureincrementCounter()asyncrookie_cruekie Автор
08.11.2019 16:47Напрашивается аналогия с козой и баяном, если не забавнее.
Неоднократно доказано, что желание везде подстелить соломки не защищает от идиотов или любознательных вредителей.
Но раз уж Вы за чистоту рядов, то что скажете про реализацию
toMap/fromMap
в дарте "из коробки"? Отсутствие как минимум признака типа в карте делает ее худо-бедно применяемой только в узком локальном контексте, не позволяя передавать никуда дальше, ибо иначе может приводить к интересным коллизиям типа
test('Когда все пошло не по плану', () async { // в точке А. final manBefore = Straight(name: 'Serg', age: 25); final map = manBefore.toMap(); print('man before is: ${manBefore.runtimeType} as $map'); // в точке Б. final manAfter = Gay.fromMap(map); print('man after is: ${manAfter.runtimeType} as $map'); }); LOG: man before is: Straight as {name: Serg, age: 25} man after is: Gay as {name: Serg, age: 25}
для схожих типов
class Straight { final String name; final int age; ... class Gay { final String name; final int age; ...
и персонаж даже не узнает об этом, пока не нагнется за мылом....
Мир, он не черно-белый, и не становитесь упоротым адептом навязанных извне технологий, даже если очень надо или сильно хочется.
Rikudoxxx
08.11.2019 12:50Реактивность это очень несложно. Гораздо проще InheritedWidgets, BLoC Provider, etc.
Насколько я могу судить то базово ты как раз таки реализовал BLoC
tetraset
08.11.2019 14:57По мне так для управления состоянием лучше всего подходит пакет: pub.dev/packages/flutter_bloc
Тут хорошо объясняется как им пользоваться: www.youtube.com/watch?v=hTExlt1nJZI
Для данного пакета уже создана целая инфраструктура:
pub.dev/packages/hydrated_bloc
pub.dev/packages/bloc_test
pub.dev/packages/sealed_flutter_blocrookie_cruekie Автор
08.11.2019 15:01Прекрасно, когда есть из чего выбрать. Мне фломастеры из
rxdart
-коробки больше по душе, потому что я так или иначе последние три года пользуюсь ReactiveX в составеRxJava
,RxQt
,RxCpp
в той или иной степени, с их подкупающей мощью обработки данных посредством цепочек операторов, и поэтому я убиваю двух зайцев, используяrxdart
как управление состоянием UI и для работы с данными.
andrew8712
И получим глобальную переменную
onCounterUpd
и глобальную функциюincrementCounter()
. Вот только зачем?rookie_cruekie Автор
Обратите внимание, это для простоты восприятия. Никто не мешает вам сделать класс, и убрать его в business-слой.