Делаем свой Pattern Matching в Dart
Как известно, полноценного механизма сопоставления шаблонов в языке Dart нет, хотя разработчики обещают его завезти.
Однако, в своих задачах я часто сталкиваюсь с ситуацией, когда этого механизма сильно не хватает. Типичный пример - работа со всевозможными билдерами (BlocBuilder
, ValueListenableBuilder
и т.д.), когда в зависимости от состояния нужно вернуть один из нескольких виджетов:
...
BlocBuilder(
bloc: bloc,
builder: (context, state) {
if (state is SomeClass) {
return SomeWidget(...);
} else if (state is OtherClass) {
return OtherWidget(...);
}
...
}
),
...
Управляться с таким набором условий совсем невесело.
В некоторых случаях бывает удобнее воспользоваться оператором switch
, хотя и в этом случае код получится весьма громоздким:
BlocBuilder(
bloc: bloc,
builder: (context, state) {
switch (state.status) {
case StatusEnum.someStatus:
return SomeWidget(...);
case StatusEnum.otherStatus:
return OtherWidget(...);
}
...
}
),
...
Всё становится ещё хуже, например, если вы хотите организовать анимацию переключения между виджетами для каждого состояния:
builder: (context, state) {
final Widget widget;
if (state.hasError) {
widget = ErrorWidget(...);
} else if (state.isLoading) {
widget = LoadingWidget(...);
} else {
widget = const SizedBox();
}
return AnimatedSwitcher(
duration: duration,
transitionBuilder: (child, animation) => FadeTransition(
opacity: animation,
child: child,
),
child: widget,
);
},
When - Case
Итак, давайте приступим к реализации механизма сопоставления шаблонов.
Общая идея такова: мы получаем коллекцию возможных вариантов, плюс вариант по-умолчанию (если ни оин шаблон не был сопоставлен); каждый вариант включает в себя условие сопоставления и результат заданного типа T
, который будет возвращён при сопоставлении.
Для начала выразим идею варианта сопоставления в виде абстрактного класса AbstractCase
:
abstract class AbstractCase<R> {
bool get condition;
R get result;
const AbstractCase();
}
Теперь можно реализовать механизм сопоставления:
class When<R> {
final Iterable<AbstractCase<R>> _cases;
final R _defaultResult;
R get match {
for (final c in _cases) {
if (c.condition) return c.result;
}
return _defaultResult;
}
const When(Iterable<AbstractCase<R>> cases, R defaultResult)
: _cases = cases,
_defaultResult = defaultResult;
}
Осталось добавить конкретные реализации AbstractCase
(их будет две):
class Case<R> extends AbstractCase<R> {
@override
final bool condition;
@override
final R result;
const Case(this.condition, this.result);
}
class LazyCase<R> extends AbstractCase<R> {
final bool Function() _condition;
final R Function() _result;
@override
bool get condition => _condition();
@override
R get result => _result();
const LazyCase(bool Function() condition, R Function() result)
: _condition = condition,
_result = result;
}
Как использовать?
Рассмотрим на примере с AnimatedSwitcher
.
В простейшем случае можно воспользоваться Case
:
builder: (context, state) {
final widget = When([
Case(state.hasError, ErrorWidget(...)),
Case(state.isLoading, LoadingWidget(...)),
],
const SizedBox(),
).match;
return AnimatedSwitcher(
duration: duration,
transitionBuilder: (child, animation) => FadeTransition(
opacity: animation,
child: child,
),
child: widget,
);
},
Правда у такого применения есть недостаток: все виджеты во втором аргументе Case
будут построены, даже если ни одно из условий не будет выполнено. Чтобы этого избежать, можно добавить немного "ленивости" в наши варианты:
builder: (context, state) {
final widget = When([
Case(state.hasError, () => ErrorWidget(...)),
Case(state.isLoading, () => LoadingWidget(...)),
],
() => const SizedBox(),
).match;
return AnimatedSwitcher(
duration: duration,
transitionBuilder: (child, animation) => FadeTransition(
opacity: animation,
child: child,
),
child: widget.call(),
);
},
Фактически, в данном случае каждый Case
вернёт не сам виджет, а функцию-билдер. Благодаря этому мы можем не хранить в памяти все результаты, а получить только сопоставленный при необходимости (расходы на лямбда-функции считаем несущественными).
Следует заметить, что если вычисление значения условия в Case
тоже оказывается вычислительно трудоёмкой задачей, то можно отложить его до момента сопоставления конкретного варианта. В этом нам поможет "ленивая" реализация LazyCase
:
builder: (context, state) {
final widget = When([
LazyCase(() => state.errorCondition(), () => ErrorWidget(...)),
LazyCase(() => state.loadingCondition(), () => LoadingWidget(...)),
],
SizedBox(),
).match;
return AnimatedSwitcher(
duration: duration,
transitionBuilder: (child, animation) => FadeTransition(
opacity: animation,
child: child,
),
child: widget,
);
},
Теперь паттерн-матчинг доступен и в Dart/Flutter, пользуйтесь на здоровье!