Irbisa · Шпаргалка
Моделирование состояния
Иммутабельность, freezed, sealed-классы, equality, оптимистичные апдейты, восстановление
Чем эфемерное (локальное) состояние отличается от состояния приложения во Flutter и как решить, где должно жить значение?
Что значит неизменяемое (immutable) состояние и почему
CubitиNotifierждут новый объект состояния при каждом изменении?class CartState { const CartState({this.items = const []}); final List<Item> items; CartState copyWith({List<Item>? items}) => CartState(items: items ?? this.items);…Как моделировать состояния загрузки, данных и ошибки: отдельными булевыми флагами или sealed-классом?
sealed class FeedState { const FeedState();} final class FeedLoading extends FeedState { const FeedLoading();…Найдите баг: после выхода офлайн-кэша пользователи с устаревшим кэшем видят пустой экран заказов.
// 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(), };}Этот виджет сортирует список, полученный от
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)], );}Как реализовать равенство по значению для класса состояния и почему это важно для ребилдов?
class FeedState { const FeedState({this.items = const [], this.loading = false}); final List<Item> items; final bool loading; @override…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];}Почему рукописные
copyWithломаются на вложенном состоянии и что вместо них даётfreezed?// 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'), ),);…Найдите баг: пользователи, которые уходят с экрана профиля, пока он ещё грузится, вызывают краш-репорт «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)); }}Почему
totalлучше хранить геттером, а не полем, и что ломается, когда экран копирует состояние вinitState?Когда связанные поля должны жить в одном объекте состояния, а не в нескольких отдельных нотифаерах?
// 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}…Зачем нормализовать состояние в мапу по 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;…Как моделировать состояние многошаговой формы и когда наполовину заполненные данные становятся настоящим доменным объектом?
class SignupDraft { String? email; String? name; // null while a required field is still missing Signup? toSignup() {…Как сделать оптимистичное обновление UI и откатить его, если запрос упал?
enum Sync { synced, pending, failed } class FeedCubit extends Cubit<List<Post>> { FeedCubit(this._api) : super(const []); final Api _api;…Пользователь дважды быстро нажимает на звёздочку. Что в итоге может показать этот
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); }}Как решить, сколько живёт холдер состояния (виджет, маршрут, сессия или приложение), и что ломается, если он живёт слишком долго?
Как работает восстановление состояния во Flutter через
RestorationMixinи что восстанавливать, а что загружать заново после того, как ОС убила приложение?class _CheckoutPageState extends State<CheckoutPage> with RestorationMixin { final _note = RestorableTextEditingController(); final _orderId = RestorableStringN(null); @override String? get restorationId => 'checkout';…Как смоделировать флоу оформления заказа в виде конечного автомата и что даёт чистая функция перехода?
sealed class CheckoutState {}final class Idle extends CheckoutState {}final class Submitting extends CheckoutState {}final class AwaitingConfirmation extends CheckoutState { AwaitingConfirmation(this.paymentId); final String paymentId;…Как версионировать сохранённое состояние, чтобы обновление приложения не падало на данных, записанных старой версией?
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,…