Command pattern və yan effektlər
Rəsmi bələdçidə command — view model-in view-a açdığı hərəkətdir. Ayrı dizayn pattern səhifəsi ona həsr olunub və üç konkret problemi həll edir:
1. İkiqat basılma. İstifadəçi düyməni sürətlə iki dəfə basır və əməliyyat iki dəfə icra olunur. Command execute() metodunda if (_running) yoxlaması ilə erkən qayıdır.
2. State-in çoxalması. Command olmadan hər hərəkət üçün ayrı sahələr lazımdır: runningLoad, errorLoad, runningEdit, errorEdit. Command bu vəziyyəti öz içində saxlayır.
3. UI hərəkətlərinin tetiklənməsi. Xəta dialoqu, snackbar, naviqasiya — command-ın state-i (running, error, completed) bunları asanlaşdırır.
Rəsmi implementasiya Command0 (arqumentsiz) və Command1<T, A> (bir arqumentli) siniflərini verir; onlar ChangeNotifier-dən miras alır və running, error, completed, result ilə clearResult() açır.
Riverpod-da ayrı Command sinfi lazım deyil: notifier-in metodu command rolunu oynayır, AsyncValue isə running/error vəziyyətlərini daşıyır. Lakin problemlər qalır — ikiqat basılma və yan effektlərin təkrarı Riverpod-da da baş verir, sadəcə həlli fərqlidir.
// Rəsmi dizayn pattern səhifəsindəki forma (sadələşdirilmiş).
typedef CommandAction0<T> = Future<Result<T>> Function();
typedef CommandAction1<T, A> = Future<Result<T>> Function(A);
abstract class Command<T> extends ChangeNotifier {
bool _running = false;
Result<T>? _result;
bool get running => _running;
bool get error => _result is Error;
bool get completed => _result is Ok;
Result<T>? get result => _result;
/// Nəticə bir dəfə istifadə olunduqdan sonra təmizlənir —
/// beləliklə snackbar/dialoq TƏKRARLANMIR.
void clearResult() {
_result = null;
notifyListeners();
}
Future<void> _execute(CommandAction0<T> action) async {
// İkiqat basılmanın qarşısı: əməliyyat gedirsə, erkən qayıdır.
if (_running) return;
_running = true;
_result = null;
notifyListeners();
try {
_result = await action();
} finally {
_running = false;
notifyListeners();
}
}
}
final class Command0<T> extends Command<T> {
Command0(this._action);
final CommandAction0<T> _action;
Future<void> execute() async => _execute(_action);
}
final class Command1<T, A> extends Command<T> {
Command1(this._action);
final CommandAction1<T, A> _action;
Future<void> execute(A argument) async =>
_execute(() => _action(argument));
}
// ── View model-də istifadə ──
class OrderViewModel extends ChangeNotifier {
OrderViewModel({required OrderRepository repository})
: _repository = repository {
load = Command0(_load);
cancel = Command1(_cancel);
}
final OrderRepository _repository;
late final Command0<List<Order>> load;
late final Command1<Order, String> cancel;
Future<Result<List<Order>>> _load() => _repository.fetchMine();
Future<Result<Order>> _cancel(String id) => _repository.cancel(id);
}
// ── View-da: ListenableBuilder command-a abunə olur ──
// ListenableBuilder(
// listenable: viewModel.load,
// builder: (context, child) {
// if (viewModel.load.running) return const CircularProgressIndicator();
// if (viewModel.load.error) return const ErrorView();
// return child!;
// },
// child: ...,
// )Rəsmi `Command` sinfinin forması — `ChangeNotifier` əsaslı layihələrdə birbaşa işlədilir.
Riverpod-da eyni problemlərin həlli. Ayrı Command sinfi yazmaq lazım deyil, lakin üç məsələni yenə həll etmək lazımdır.
1. İkiqat basılma. İki üsul:
- State-də
isSubmittingbayrağı (ya da sealed union-dasubmittingvariantı) və metodun başında yoxlama. - View tərəfində düyməni deaktiv etmək:
onPressed: state.isSubmitting ? null : () => .... Bu, hər iki tərəfdə edilməlidir — düymə deaktiv olsa da, klaviatura ya da başqa yol qalır.
2. Yan effektlərin təkrarlanması. Naviqasiya, snackbar, dialoq, analitika hadisəsi bir dəfə baş verməlidir. Problem build-in təbiətindən doğur: o, istənilən anda təkrar çağırıla bilər. Riverpod-da doğru yer:
ref.listen(provider, (prev, next) { ... })—buildiçində təhlükəsizdir, yalnız dəyişiklikdə işləyir.- Ya da: notifier-in metodu nəticə qaytarır, view isə
await-dan sonra effekti icra edir. Bu variant daha aydındır, çünki səbəb-nəticə bir yerdə görünür.
3. `context.mounted` yoxlaması. Async əməliyyatdan sonra widget ağacdan silinmiş ola bilər. await-dan sonra context işlədən hər yerdə if (!context.mounted) return; lazımdır — bu, ən çox buraxılan yoxlamadır və məhsul rejimində "Looking up a deactivated widget's ancestor" xətası kimi görünür.
// ══ Notifier: command + ikiqat basılma müdafiəsi ══
@riverpod
class CheckoutNotifier extends _$CheckoutNotifier {
@override
CheckoutState build() =>
CheckoutState.editing(form: CheckoutForm.empty());
Future<SubmitOutcome> submit() async {
// 1) Müdafiə: yalnız editing/failed vəziyyətindən başlayır.
final form = switch (state) {
CheckoutEditing(:final form) => form,
CheckoutFailed(:final form) => form,
CheckoutSubmitting() => null, // artıq gedir → təkrar yox
CheckoutSubmitted() => null, // artıq bitib
};
if (form == null) return SubmitOutcome.ignored;
state = CheckoutState.submitting(form: form);
final result = await ref.read(orderRepositoryProvider).place(form);
switch (result) {
case Ok(:final value):
state = CheckoutState.submitted(order: value);
// 2) Nəticə qaytarılır — naviqasiya BURADA edilmir.
return SubmitOutcome.success(orderId: value.id);
case Error(error: ValidationFailure(:final fieldErrors)):
state = CheckoutState.editing(form: form, fieldErrors: fieldErrors);
return SubmitOutcome.invalid;
case Error(:final error):
state = CheckoutState.failed(form: form, failure: error as Failure);
return SubmitOutcome.failed;
}
}
}
// ══ View: yan effekt bir dəfə, mounted yoxlaması ilə ══
class CheckoutPage extends ConsumerWidget {
const CheckoutPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(checkoutNotifierProvider);
final isSubmitting = state is CheckoutSubmitting;
return Scaffold(
body: CheckoutFormView(state: state),
bottomNavigationBar: FilledButton(
// 3) Düymə də deaktiv edilir — iki səviyyəli müdafiə.
onPressed: isSubmitting ? null : () => _submit(context, ref),
child: isSubmitting
? const CircularProgressIndicator()
: Text(AppLocalizations.of(context).submitOrder),
),
);
}
Future<void> _submit(BuildContext context, WidgetRef ref) async {
final outcome =
await ref.read(checkoutNotifierProvider.notifier).submit();
// 4) `await`-dan SONRA context işlədilir → mounted yoxlanılır.
if (!context.mounted) return;
final l10n = AppLocalizations.of(context);
switch (outcome) {
case SubmitSuccess(:final orderId):
// Naviqasiya view-da: bir dəfə, açıq şəkildə.
context.go('/orders/$orderId?justCreated=true');
case SubmitOutcome.invalid:
// Sahə xətaları artıq state-dədir; əlavə snackbar lazım deyil.
break;
case SubmitOutcome.failed:
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.submitFailed)),
);
case SubmitOutcome.ignored:
break;
}
}
}
// ══ Alternativ: ref.listen ilə (state dəyişikliyinə reaksiya) ══
// build içində:
// ref.listen(checkoutNotifierProvider, (previous, next) {
// if (next case CheckoutSubmitted(:final order)) {
// context.go('/orders/${order.id}');
// }
// });
// Bu variant "state-ə görə naviqasiya" məntiqi üçün uyğundur
// (deep link, avtomatik yönləndirmə); düymə basılması üçün isə
// yuxarıdaki `await` variantı daha aydındır.Riverpod-da command: ikiqat basılmanın qarşısı, nəticənin qaytarılması və yan effektin bir dəfə icrası.
Yan effekti `build`-də icra etməmək. Ən təhlükəli səhv budur:
if (state.isSubmitted) { context.go('/success'); } — build-in gövdəsində.
build istənilən anda təkrar çağırıla bilər (ata widget rebuild oldu, klaviatura açıldı, ekran döndü). Nəticə: iki naviqasiya, üst-üstə düşən dialoqlar, təkrarlanan analitika hadisələri. Bəzən daha pisi — build zamanı naviqasiya Flutter-in assert-ini pozur.
Doğru yerlər: ref.listen (Riverpod), callback-in içi (onPressed → await → effekt), ya da addPostFrameCallback (nadir hallarda).
Praktika. Bir yazma əməliyyatını (submit ya da delete) tam qur: notifier-də ikiqat basılma müdafiəsi, nəticə enum-u, view-da context.mounted yoxlaması və uğur/xəta üçün fərqli davranış.
Sonra sına: düyməni sürətlə üç dəfə bas. Hazır sayılır: şəbəkəyə bir sorğu gedir (loqda görünür), bir snackbar görünür və naviqasiya bir dəfə baş verir.
📚 Mənbələr və sənədlər
- Command pattern-irəsmidocs.flutter.dev
Bu mövzunun mənbəyi: `Command0`/`Command1`, `running`/`error`/`completed`, `clearResult()` və ikiqat basılma müdafiəsi.
- Riverpod: ref qaydalarırəsmiriverpod.dev
`ref.listen` — yan effekt üçün doğru yer; `ref.read` isə command-ların içində.
- Optimistic state pattern-irəsmidocs.flutter.dev
Yazma nəticəsini gözləmədən UI-ı yeniləmək və uğursuzluqda geri qaytarmaq.
- Arxitektura tövsiyələri: commandrəsmidocs.flutter.dev
Command-ların niyə tövsiyə olunduğu: render xətalarının qarşısı və hadisələrin standartlaşdırılması.