UI state-in modelləşdirilməsi: AsyncValue, status enum, sealed union
Ekranın state-i domain modeli deyil. Domain Order obyektini verir; ekran isə əlavə olaraq bilməlidir: yüklənir, xəta var, filtr seçilib, submit gedir, siyahı boşdur.
Bu state-i modelləşdirməyin üç yolu var və hər birinin öz yeri var.
1. `AsyncValue<T>` (Riverpod-un hazır tipi). AsyncLoading, AsyncError, AsyncData — üç vəziyyət bir tipdə. Riverpod 3-də sealed olduğu üçün switch kompilyator tərəfindən yoxlanılır. Sadə "yüklə və göstər" ekranları üçün ən qısa yoldur: əlavə sinif yazmaq lazım gəlmir.
2. Status `enum` + sahələr. Bir sinif, bir enum status, sahələr nullable. Bloc sənədləri bu yanaşmayı vəziyyətlər bir-birini tam istisna etmədikdə və çoxlu paylaşılan sahə olduqda tövsiyə edir — məsələn siyahı göstərilir və eyni anda yenilənir.
3. `sealed` union (freezed ilə). Hər vəziyyət ayrı tipdir: Editing, Submitting, Submitted, Failed. Vəziyyətlər bir-birini istisna etdikdə uyğundur və mümkün olmayan state-ləri struktur olaraq qadağan edir.
Seçim meyarı bir sualdır: "bu iki vəziyyət eyni anda ola bilərmi?" Ola bilmirsə — sealed union; ola bilirsə — status enum ya da AsyncValue.
// ══ 1. AsyncValue: sadə "yüklə və göstər" ══
@riverpod
class OrderList extends _$OrderList {
@override
Future<List<Order>> build() =>
ref.watch(orderRepositoryProvider).fetchMine();
}
// View: AsyncValue sealed-dir, switch tam yoxlanılır.
final state = ref.watch(orderListProvider);
return switch (state) {
AsyncData(:final value) when value.isEmpty => const EmptyOrders(),
AsyncData(:final value) => OrderListView(orders: value),
AsyncError(:final error) => ErrorView(error: error),
_ => const LoadingView(),
};
// ══ 2. Status enum: vəziyyətlər üst-üstə düşür ══
// (siyahı göstərilir VƏ eyni anda yenilənir VƏ filtr seçilib)
enum OrderListStatus { initial, loading, refreshing, success, failure }
@freezed
abstract class OrderListState with _$OrderListState {
const factory OrderListState({
@Default(OrderListStatus.initial) OrderListStatus status,
@Default([]) List<Order> orders, // yenilənərkən köhnə siyahı qalır
@Default(OrderFilter.all) OrderFilter filter,
Failure? failure,
@Default(false) bool hasMore, // səhifələmə
}) = _OrderListState;
}
// ══ 3. Sealed union: vəziyyətlər bir-birini istisna edir ══
// (form: ya redaktə olunur, ya göndərilir, ya bitib)
@freezed
sealed class CheckoutState with _$CheckoutState {
const factory CheckoutState.editing({
required CheckoutForm form,
@Default({}) Map<String, String> fieldErrors,
}) = CheckoutEditing;
const factory CheckoutState.submitting({
required CheckoutForm form,
}) = CheckoutSubmitting;
const factory CheckoutState.submitted({
required Order order,
}) = CheckoutSubmitted;
const factory CheckoutState.failed({
required CheckoutForm form,
required Failure failure,
}) = CheckoutFailed;
}
// View: hər vəziyyət ayrı widget, mümkün olmayan hal YOXDUR.
return switch (ref.watch(checkoutNotifierProvider)) {
CheckoutEditing(:final form, :final fieldErrors) =>
CheckoutForm(form: form, errors: fieldErrors),
CheckoutSubmitting() => const CheckoutFormDisabled(),
CheckoutSubmitted(:final order) => OrderSuccess(order: order),
CheckoutFailed(:final form, :final failure) =>
CheckoutForm(form: form, banner: failure),
};Eyni ekran üç modelləşdirmə ilə. Diqqət et: birinci variantda öz state sinifin yoxdur — bu, əksər oxuma ekranları üçün kifayətdir.
| Meyar | `AsyncValue<T>` | Status enum + sahələr | `sealed` union |
|---|---|---|---|
| Öz state sinfin lazımdır? | Xeyr | Bəli (1 sinif) | Bəli (1 sinif + N variant) |
| Mümkün olmayan state | Struktur olaraq qadağandır | Mümkündür — nullable sahələr | Struktur olaraq qadağandır |
| Yenilənərkən köhnə məlumat | Var (`AsyncValue` dəyəri saxlayır) | Var — təbii şəkildə | Əlavə variant lazımdır (`refreshing(data)`) |
| Bir neçə əlavə sahə (filtr, səhifə) | Çətin — `AsyncValue<(List, Filter)>` oxunmur | Rahat | Hər variantda təkrarlanır |
| Ən uyğun yer | Oxuma ekranları (siyahı, detal) | Filtr/səhifələmə/axtarış olan siyahılar | Formalar, çox addımlı axınlar, wizard |
Mümkün olmayan state nədir. Ən çox rast gəlinən nümunə budur:
bool isLoading+Failure? failure+List<Order> orders
Bu üç sahə 8 kombinasiya yaradır, halbuki onların yalnız 4-ü mənalıdır. isLoading == true && failure != null nə deməkdir? Cavab yoxdur — lakin kod bu vəziyyəti yaratmağa icazə verir və nə vaxtsa yaradır. Nəticə: ekranda spinner və xəta mesajı üst-üstə düşür, ya da heç nə görünmür.
AsyncValue və sealed union bu problemi tipin özündə həll edir: belə kombinasiya yaratmaq mümkün deyil.
Vacib istisna. Bəzi "qeyri-mümkün" görünən kombinasiyalar əslində lazımdır: məlumat var və eyni anda yenilənir (pull-to-refresh). AsyncValue bunu dəstəkləyir — yenilənmə zamanı əvvəlki dəyər saxlanılır, ona görə ekran boşalmır. Sealed union-da isə bunun üçün ayrıca variant (refreshing(data)) yazmaq lazımdır.
Ona görə "hər şeyi sealed union et" qaydası səhvdir: siyahı ekranları üçün status enum ya da AsyncValue daha az kod və daha az variant deməkdir.
State sinfi nə saxlamır. Üç şey ekranın state-inə düşməməlidir:
- Widget-ə aid obyektlər:
TextEditingController,ScrollController,AnimationController,GlobalKey. Onların yeriStatefulWidget-dir. - Göstərmə formatı:
String formattedTotal,Color statusColor. Statedouble totalvəOrderStatussaxlayır; format və rəngbuild-də hesablanır. - `BuildContext`. Heç bir formada.
Praktika. Layihəndən bir ekran seç və onun state-ini yaz: əvvəlcə bütün mümkün vəziyyətləri kağızda sadala (loading, boş, məlumat, yenilənir, xəta, submit gedir...), sonra "bunlar eyni anda ola bilərmi?" sualına görə modelləşdirmə üsulunu seç.
Hazır sayılır: switch bütün halları əhatə edir, default yoxdur, və state sinfində nə Controller, nə Color, nə BuildContext var.
📚 Mənbələr və sənədlər
- Case study: UI qatırəsmidocs.flutter.dev
View model-in ekran üçün state saxlaması və view-un ona necə abunə olması.
- Riverpod: AsyncValue (API sənədi)rəsmipub.dev
`AsyncData`, `AsyncError`, `AsyncLoading` alt tipləri və `value`, `hasError`, `isRefreshing` üzvləri — sealed tipin rəsmi API-si.
- freezed: union tiplərirəsmipub.dev
`@freezed sealed class ... with _$X` sintaksisi və Dart `switch`-i ilə pattern matching.
- Dart: pattern-lərrəsmidart.dev
`AsyncData(:final value) when value.isEmpty` kimi guard-lı pattern-lər.