Irbisa · Шпаргалка

Моделирование состояния

Иммутабельность, freezed, sealed-классы, equality, оптимистичные апдейты, восстановление

Middle-разработчик · 19 вопросов

  1. 01

    Чем эфемерное (локальное) состояние отличается от состояния приложения во Flutter и как решить, где должно жить значение?

    Эфемерное состояние принадлежит одному виджету и умирает вместе с ним (фокус поля, выбранная вкладка, анимация).

  2. 02

    Что значит неизменяемое (immutable) состояние и почему Cubit и Notifier ждут новый объект состояния при каждом изменении?

    Неизменяемое состояние никогда не меняют на месте: каждое обновление создаёт новый объект, обычно через copyWith.

    class CartState {  const CartState({this.items = const []});  final List<Item> items;   CartState copyWith({List<Item>? items}) =>      CartState(items: items ?? this.items);…
  3. 03

    Как моделировать состояния загрузки, данных и ошибки: отдельными булевыми флагами или sealed-классом?

    sealed-классом с подтипом на каждое состояние, где каждый несёт только свои данные.

    sealed class FeedState {  const FeedState();} final class FeedLoading extends FeedState {  const FeedLoading();…
  4. 04

    Найдите баг: после выхода офлайн-кэша пользователи с устаревшим кэшем видят пустой экран заказов.

    // OrdersState is sealed: OrdersLoading, OrdersLoaded, OrdersFailed, OrdersStale@overrideWidget build(BuildContext context) {  return switch (context.watch<OrdersCubit>().state) {    OrdersLoading() => const Center(child: CircularProgressIndicator()),    OrdersLoaded(:final items) => OrderList(items),    OrdersFailed(:final message) => ErrorView(message),    _ => const SizedBox.shrink(),  };}

    Wildcard _ ловит каждое состояние, которое switch не назвал, поэтому OrdersStale попадает в SizedBox.shrink() и ничего не рисует.

  5. 05

    Этот виджет сортирует список, полученный от Cubit, прямо перед сборкой. Что ломается и почему final этому не помешал?

    class FeedState {  const FeedState(this.items);  final List<Item> items;} @overrideWidget build(BuildContext context) {  final state = context.watch<FeedCubit>().state;  state.items.sort((a, b) => b.date.compareTo(a.date));  return ListView(    children: [for (final item in state.items) ItemTile(item)],  );}

    sort() переставляет собственный список Cubit на месте, поэтому выпущенное состояние меняется в обход Cubit, и новый порядок видят все, кто его держит.

  6. 06

    Как реализовать равенство по значению для класса состояния и почему это важно для ребилдов?

    Переопределите == и hashCode, чтобы два состояния с одинаковыми полями были равны (вручную, через Equatable или с генерацией через freezed).

    class FeedState {  const FeedState({this.items = const [], this.loading = false});  final List<Item> items;  final bool loading;   @override…
  7. 07

    Cubit меняет onlyInStock при каждом нажатии на переключатель, но список перестаёт реагировать. Что не так с этим классом состояния?

    class FilterState extends Equatable {  const FilterState({this.query = '', this.onlyInStock = false});  final String query;  final bool onlyInStock;   FilterState copyWith({String? query, bool? onlyInStock}) => FilterState(        query: query ?? this.query,        onlyInStock: onlyInStock ?? this.onlyInStock,      );   @override  List<Object?> get props => [query];}

    onlyInStock нет в props, поэтому состояние, которое отличается только этим флагом, == текущему.

  8. 08

    Почему рукописные copyWith ломаются на вложенном состоянии и что вместо них даёт freezed?

    На каждом уровне нужно перечислить все поля, поэтому вложенное обновление превращается в цепочку вызовов copyWith, где одно забытое поле молча сохраняет старое значение, а передача null ничего не очищает.

    // wrong: hand-written, three levels deep, and null cannot clear a fieldfinal next = state.copyWith(  user: state.user.copyWith(    address: state.user.address.copyWith(city: 'Almaty'),  ),);…
  9. 09

    Найдите баг: пользователи, которые уходят с экрана профиля, пока он ещё грузится, вызывают краш-репорт «Bad state: Cannot emit new states after calling close».

    class ProfileCubit extends Cubit<ProfileState> {  ProfileCubit(this._api) : super(ProfileLoading());  final ProfileApi _api;   Future<void> load() async {    final profile = await _api.fetch();    emit(ProfileLoaded(profile));  }}

    load() ждёт ответ сети и затем вызывает emit, но к моменту ответа BlocProvider мог уже закрыть cubit, потому что экран закрыли.

  10. 10

    Почему total лучше хранить геттером, а не полем, и что ломается, когда экран копирует состояние в initState?

    Производные значения вычисляют из состояния и не хранят, а экран читает показываемое состояние, а не копирует его.

  11. 11

    Когда связанные поля должны жить в одном объекте состояния, а не в нескольких отдельных нотифаерах?

    Когда они меняются вместе или должны быть согласованы друг с другом.

    // wrong: three notifiers, three notificationsvoid applyFilter(Filter f) {  filter.value = f;          // a listener here still sees page 4  page.value = 1;            // and here the old selection  selected.value = const {}; // only now consistent}…
  12. 12

    Зачем нормализовать состояние в мапу по id вместо того, чтобы вкладывать одни и те же объекты в несколько списков?

    Чтобы каждая сущность хранилась один раз, а списки держали только id.

    class FeedState {  const FeedState({this.posts = const {}, this.feed = const [], this.saved = const []});   final Map<PostId, Post> posts; // each post stored once  final List<PostId> feed;       // lists hold only ids  final List<PostId> saved;…
  13. 13

    Как моделировать состояние многошаговой формы и когда наполовину заполненные данные становятся настоящим доменным объектом?

    Держите один черновик, которым владеет весь флоу, с nullable-полями без валидации, и превращайте его в провалидированный доменный объект один раз, при отправке.

    class SignupDraft {  String? email;  String? name;   // null while a required field is still missing  Signup? toSignup() {…
  14. 14

    Как сделать оптимистичное обновление UI и откатить его, если запрос упал?

    Сразу примените изменение к состоянию, пометьте элемент как ожидающий, отправьте запрос, а при ошибке отмените ровно это изменение и сообщите пользователю.

    enum Sync { synced, pending, failed } class FeedCubit extends Cubit<List<Post>> {  FeedCubit(this._api) : super(const []);  final Api _api;…
  15. 15

    Пользователь дважды быстро нажимает на звёздочку. Что в итоге может показать этот Cubit и как сделать результат детерминированным?

    class FavoriteCubit extends Cubit<bool> {  FavoriteCubit(this._api, bool initial) : super(initial);  final Api _api;   Future<void> toggle() async {    final next = !state;    await _api.setFavorite(next);    emit(next);  }}

    Оба нажатия читают state до прихода любого ответа, поэтому оба отправляют true, и после двух нажатий звёздочка остаётся включённой.

  16. 16

    Как решить, сколько живёт холдер состояния (виджет, маршрут, сессия или приложение), и что ломается, если он живёт слишком долго?

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

  17. 17

    Как работает восстановление состояния во Flutter через RestorationMixin и что восстанавливать, а что загружать заново после того, как ОС убила приложение?

    Задайте restorationScopeId в MaterialApp, подмешайте RestorationMixin в State и зарегистрируйте Restorable*-свойства в restoreState; ОС сохранит их и вернёт после смерти процесса.

    class _CheckoutPageState extends State<CheckoutPage> with RestorationMixin {  final _note = RestorableTextEditingController();  final _orderId = RestorableStringN(null);   @override  String? get restorationId => 'checkout';…
  18. 18

    Как смоделировать флоу оформления заказа в виде конечного автомата и что даёт чистая функция перехода?

    Сделайте каждый шаг подтипом sealed-класса состояния, каждый входной сигнал сделайте подтипом sealed-класса события и напишите одну синхронную next(state, event), которая возвращает новое состояние.

    sealed class CheckoutState {}final class Idle extends CheckoutState {}final class Submitting extends CheckoutState {}final class AwaitingConfirmation extends CheckoutState {  AwaitingConfirmation(this.paymentId);  final String paymentId;…
  19. 19

    Как версионировать сохранённое состояние, чтобы обновление приложения не падало на данных, записанных старой версией?

    Сохраняйте номер версии рядом с данными, мигрируйте пошагово от любой старой версии до текущей, а если что-то не парсится, удаляйте сохранённые данные и стартуйте с начального состояния вместо падения.

    const _version = 3; final _migrations = <int, Map<String, dynamic> Function(Map<String, dynamic>)>{  1: (json) => {...json, 'plan': 'free'},          // v1 -> v2: new field  2: (json) => {                                   // v2 -> v3: renamed field        ...json,…