Стейт-менеджмент — это способ управления состоянием приложения. Всё, что пользователь видит на экране — это состояние (State
): цвет кнопки, текст, загруженные данные, анимации. Даже пустая страница — это состояние.
Во Flame
компоненты в основном — это просто описания UI, а их отображение зависит от переданного состояния. Задача стейт-менеджмента — эффективно управлять этим состоянием, чтобы приложение оставалось предсказуемым и производительным.
Почему BLoC
— один из лучших подходов?
Теперь, когда понятно, зачем нужен стейт-менеджмент, разберём BLoC (Business Logic Component)
— одну из самых популярных библиотек для управления состоянием.
Ключевые идеи BLoC
:
Всё, что видит пользователь — это State
.
Например: GameStatusState
может содержать в себе поля счета, жизней и статуса игры, который представлен enum
: initial
, respawn
, respawned
, gameOver
.
Изменения происходят через Events
.
Пользователь нажал кнопку? Это событие (Event).
Данные загрузились из сети? Это тоже Event.
BLoC — это "менеджер состояний".
Он принимает Event
, обрабатывает его и выдаёт новый State
.
Как выглядит схема работы?
Добавляем Event
→ BLoC
обрабатывает Event
→ BLoC
возвращает новый State
.
Этот цикл повторяется на протяжении всей работы приложения.
Также у нас могут быть какие-то источники информации, например база данных или сервер. Bloc позволяет делать запросы в это время к источнику данных и, получив ответ, вернуть новое состояние.
Пример с flame_bloc
(игра на Flame + BLoC
)
Допустим, у нас есть игра, и мы управляем её состоянием через BLoC
:
enum GameStatus {
initial,
respawn,
respawned,
gameOver,
}
class GameStatsState extends Equatable {
final int score;
final int lives;
final GameStatus status;
const GameStatsState({
required this.score,
required this.lives,
required this.status,
});
const GameStatsState.empty()
: this(
score: 0,
lives: 3,
status: GameStatus.initial,
);
GameStatsState copyWith({
int? score,
int? lives,
GameStatus? status,
}) {
return GameStatsState(
score: score ?? this.score,
lives: lives ?? this.lives,
status: status ?? this.status,
);
}
@override
List<Object?> get props => [score, lives, status];
}
abstract class GameStatsEvent extends Equatable {
const GameStatsEvent();
}
class ScoreEventAdded extends GameStatsEvent {
const ScoreEventAdded(this.score);
final int score;
@override
List<Object?> get props => [score];
}
class PlayerDied extends GameStatsEvent {
const PlayerDied();
@override
List<Object?> get props => [];
}
class PlayerRespawned extends GameStatsEvent {
const PlayerRespawned();
@override
List<Object?> get props => [];
}
class GameReset extends GameStatsEvent {
const GameReset();
@override
List<Object?> get props => [];
}
class GameStatsBloc extends Bloc<GameStatsEvent, GameStatsState> {
GameStatsBloc() : super(const GameStatsState.empty()) {
on<ScoreEventAdded>(
(event, emit) => emit(
state.copyWith(score: state.score + event.score),
),
);
on<PlayerRespawned>(
(event, emit) => emit(
state.copyWith(status: GameStatus.respawned),
),
);
on<PlayerDied>((event, emit) {
if (state.lives > 1) {
emit(
state.copyWith(
lives: state.lives - 1,
status: GameStatus.respawn,
),
);
} else {
emit(
state.copyWith(
lives: 0,
status: GameStatus.gameOver,
),
);
}
});
on<GameReset>(
(event, emit) => emit(
const GameStatsState.empty(),
),
);
}
}
Как это работает на практике?
Пользователь нажимает кнопку "Пауза" → отправляется PauseGame event.
BLoC
получает событие → меняет состояние на GameState.paused
.
Интерфейс автоматически обновляется (например, появляется меню паузы).
Почему BLoC
(flame_bloc
) — это удобно?
✅ Чёткое разделение логики и UI
– бизнес-логика в BLoC
, виджеты только отображают State
.
✅ Предсказуемость – состояние меняется только через Events
.
✅ Масштабируемость – легко добавлять новые Events
и States
.
✅ Тестируемость – BLoC
можно тестировать отдельно от интерфейса.
Вывод
State
– то, что видит пользователь.
Event
– действие, которое меняет State
.
BLoC
– "мозг" приложения, который управляет переходами между состояниями.
flame_bloc
— это библиотека-мост для использования Bloc
в вашей игре на Flame
. flame_bloc
предоставляет простой и естественный (аналогично flutter_bloc
) способ использования bloc
и cubit
внутри FlameGame
. Bloc
позволяет сделать изменения состояния игры предсказуемыми, регулируя, когда может произойти изменение состояния, и предоставляет единый способ изменения состояния игры во всем приложении.
Чтобы использовать его в своей игре, достаточно добавить flame_bloc
в ваш pubspec.yaml
, как показано в примере Flame Bloc
и в инструкциях по установке на pub.dev.
Предположим, у нас есть bloc, отвечающий за инвентарь игрока. Сначала нам нужно сделать его доступным для наших компонентов.
Это можно сделать с помощью компонента FlameBlocProvider
:
class MyGame extends FlameGame {
@override
Future<void> onLoad() async {
await add(
FlameBlocProvider<PlayerInventoryBloc, PlayerInventoryState>(
create: () => PlayerInventoryBloc(),
children: [
Player(),
// ...
],
),
);
}
}
С этими изменениями компонент Player
теперь будет иметь доступ к нашему bloc
.
Если нужно предоставить несколько bloc
, можно использовать FlameMultiBlocProvider
аналогичным образом:
class MyGame extends FlameGame {
@override
Future<void> onLoad() async {
await add(
FlameMultiBlocProvider(
providers: [
FlameBlocProvider<PlayerInventoryBloc, PlayerInventoryState>(
create: () => PlayerInventoryBloc(),
),
FlameBlocProvider<PlayerStatsBloc, PlayerStatsState>(
create: () => PlayerStatsBloc(),
),
],
children: [
Player(),
// ...
],
),
);
}
}
Прослушивание изменений состояния на уровне компонента
Есть два подхода для прослушивания изменений состояния:
Использование компонента FlameBlocListener
:
class Player extends PositionComponent {
@override
Future<void> onLoad() async {
await add(
FlameBlocListener<PlayerInventoryBloc, PlayerInventoryState>(
listener: (state) {
updateGear(state);
},
),
);
}
}
Использование миксина FlameBlocListenable
:
class Player extends PositionComponent
with FlameBlocListenable<PlayerInventoryBloc, PlayerInventoryState> {
@override
void onNewState(state) {
updateGear(state);
}
}
Простое чтение bloc
Если вашему компоненту нужно только получить доступ к bloc
, можно применить миксин FlameBlocReader:
class Player extends PositionComponent
with FlameBlocReader<PlayerStatsBloc, PlayerStatsState> {
void takeHit() {
bloc.add(const PlayerDamaged());
}
}
Примечание: ограничение миксина в том, что он может обращаться только к одному bloc.
FlameBlocProvider
— это компонент, который создает и предоставляет bloc своим дочерним компонентам.
Bloc будет существовать только пока жив этот компонент. Он используется как виджет для внедрения зависимостей (DI), чтобы единый экземпляр bloc
мог быть предоставлен нескольким компонентам внутри поддерева.
FlameBlocProvider
следует использовать для создания новых bloc'ов, которые будут доступны остальной части поддерева:
FlameBlocProvider<BlocA, BlocAState>(
create: () => BlocA(),
children: [...]
);
Также FlameBlocProvider
можно использовать для предоставления существующего bloc
'а новой части дерева компонентов:
FlameBlocProvider<BlocA, BlocAState>.value(
value: blocA,
children: [...],
);
Аналогичен FlameBlocProvider
, но предоставляет несколько bloc
'ов в дерево компонентов:
FlameMultiBlocProvider(
providers: [
FlameBlocProvider<BlocA, BlocAState>(
create: () => BlocA(),
),
FlameBlocProvider<BlocB, BlocBState>.value(
create: () => BlocB(),
),
],
children: [...],
);
FlameBlocListener
— это компонент, который может отслеживать изменения состояния bloc
. Он вызывает onNewState
в ответ на изменения состояния в bloc
. Для точного контроля над вызовом функции onNewState
можно использовать необязательный параметр listenWhen
. listenWhen
принимает предыдущее и текущее состояния bloc
и возвращает boolean
. Если listenWhen
возвращает true
, onNewState
будет вызван с новым состоянием. Если false
— не будет.
Альтернативно можно использовать миксин FlameBlocListenable
для отслеживания изменений состояния в компоненте:
FlameBlocListener<GameStatsBloc, GameStatsState>(
listenWhen: (previousState, newState) {
// возвращает true/false, определяя, нужно ли вызывать listener
},
onNewState: (state) {
// выполнить действия на основе состояния
},
);
FlameBlocListenable
— альтернатива FlameBlocListener
для отслеживания изменений состояния:
class ComponentA extends Component
with FlameBlocListenable<BlocA, BlocAState> {
@override
bool listenWhen(PlayerState previousState, PlayerState newState) {
// возвращает true/false, определяя, нужно ли вызывать listener
}
@override
void onNewState(PlayerState state) {
super.onNewState(state);
// выполнить действия на основе состояния
}
}
FlameBlocReader
— это миксин, позволяющий читать текущее состояние bloc
в компоненте. Полезен для компонентов, которым нужно только читать текущее состояние bloc
или отправлять события. В одном компоненте может быть только один FlameBlocReader
:
class InventoryReader extends Component
with FlameBlocReader<InventoryBloc, InventoryState> {}
/// Внутри игры:
final component = InventoryReader();
// Чтение текущего состояния:
var state = component.bloc.state;
Каждой команде необходимо выбрать игру, которую они будут разрабатывать с 5 апреля по 5 мая. План занятий следующий:
В каждой игре должно быть:
Также будет представлен набор минимальных требований от игры и дополнительных функций. Необходимо реализовать весь минимальный набор и одну-две (можно и больше если успеете) дополнительную функцию. Универсальные идеи дополнительных функций для любой из игр представлены в конце.
Минимально:
Создать игровое поле (например, 10x10 клеток).
Расставить мины случайным образом (например, 10 мин).
Реализовать клики по клеткам:
Показывать число мин вокруг открытой клетки.
Условие победы: все неминованные клетки открыты.
Условие поражения: попал на мину.
Дополнительно:
Настройки сложности (размер поля, количество мин).
Таймер и счётчик оставшихся флажков.
Открытие окружающих клеток, если вокруг открытой клетки нет ни одной мины
Анимации взрыва, звуки.
Минимально:
Дополнительно:
Изменение скорости игры через настройки или экран перед запуском игры (плюсом к этому будет увеличение счета: на 5 скорости за еду будет 5 очков, а на 1 - 1).
Временные бонусы (временная неуязвимость, двойные очки).
Система рекордов.
Разные типы еды (большая, маленькая).
Минимально:
Создать поле (например, 8x8) с разноцветными элементами.
Возможность менять местами два соседних элемента.
Проверка на совпадение 3+ одинаковых элементов по горизонтали/вертикали.
Удаление совпадений и заполнение пустот новыми элементами.
Подсчёт очков за удалённые элементы.
Дополнительно:
Специальные элементы (бомбы, линии) при совпадении 4+ элементов.
Цели уровня (набрать определённое количество очков, собрать элементы).
Ограничение по времени или количеству ходов.
Анимации взрывов и падения элементов.
Минимально:
Создать 3 вертикальных стержня.
Разместить N колец (например, 3–5) на первом стержне в порядке убывания размера (самое большое внизу).
Реализовать перетаскивание колец между стержнями с проверкой правил:
Нельзя класть большее кольцо на меньшее.
За один ход можно переместить только одно верхнее кольцо.
Условие победы: все кольца перенесены на указанный стержень (обычно третий).
Дополнительно:
Подсчёт минимального числа ходов и сравнение с рекордом игрока.
Отмена последнего хода.
Анимации для колец, звуковые эффекты.
История решений для анализа стратегии.