View-un qaydaları: widget-də hansı məntiq qala bilər
Rəsmi tövsiyələr "widget-də məntiq saxlamamaq" bəndini ən yüksək prioritetdə verir — lakin tam qadağa deyil. Bələdçi icazə verilən istisnaları konkret sadalayır. Widget-də qala bilər:
- Sadə `if` şərtləri — view model-in bayrağına görə widget göstərmək ya da gizlətmək.
- Animasiya məntiqi — widget hesablamalarına söykənən animasiyalar.
- Cihaz məlumatına görə layout — ekran ölçüsü, orientasiya, platforma.
- Sadə routing məntiqi.
Bu siyahının praktik faydası: code review-da "bu kod widget-də qala bilərmi?" sualına obyektiv cavab verməyə imkan yaradır. Siyahıda yoxdursa, view model-ə köçür.
Qadağan olunan (yəni view model-ə aid olan) şeylərə misal:
- Məlumatla bağlı hər hansı məntiq: süzgəc, sıralama, hesablama, qruplaşdırma.
- Xəta növünə görə qərar: retry göstərmək, yönləndirmək.
- Validasiya qaydaları (formanın
validator-u sadə formatı yoxlaya bilər, lakin biznes qaydası yox). - Async əməliyyatın idarəsi:
Futuregözləmək, nəticəni saxlamaq.
| Kod | Widget-də? | Səbəb |
|---|---|---|
| `if (state.isAdmin) AdminPanel()` | ✅ Olar | Bayraq view model-dən gəlir; widget yalnız göstərir |
| `if (constraints.maxWidth > 600) Row() else Column()` | ✅ Olar | Cihaz məlumatına görə layout — icazəli istisna |
| `AnimationController` və `Tween` hesablamaları | ✅ Olar | Animasiya widget-in ömrünə bağlıdır |
| `orders.where((o) => o.status.isActive).toList()` | ❌ Olmaz | Məlumat məntiqidir; hər rebuild-də yenidən hesablanır və test edilə bilmir |
| `if (error is NetworkFailure) showRetry()` | ⚠️ Sərhəd | Qərar presentation qatına aiddir, lakin `presentFailure(...)` kimi ayrı funksiyada saxlanmalıdır |
| `await repository.fetchMine()` | ❌ Olmaz | Data qatına birbaşa müraciət; view model tamamilə keçilir |
| `total * 1.18` (ƏDV hesablanması) | ❌ Olmaz | Biznes qaydası — domain modelinə ya da use-case-ə aiddir |
| `NumberFormat.currency(...).format(total)` | ✅ Olar | Göstərmə formatıdır; lokala bağlıdır və UI qatına aiddir |
// ══════ ❌ MƏNTİQLİ WIDGET ══════
class BadOrdersPage extends ConsumerStatefulWidget {
const BadOrdersPage({super.key});
@override
ConsumerState<BadOrdersPage> createState() => _BadOrdersPageState();
}
class _BadOrdersPageState extends ConsumerState<BadOrdersPage> {
OrderFilter _filter = OrderFilter.all; // ⚠️ state widget-də
bool _loading = false;
List<Order> _orders = [];
@override
void initState() {
super.initState();
_load(); // ⚠️ async idarəsi widget-də
}
Future<void> _load() async {
setState(() => _loading = true);
// ⚠️ data qatına birbaşa müraciət — view model keçilir
_orders = await ref.read(orderRepositoryProvider).fetchMine();
setState(() => _loading = false);
}
@override
Widget build(BuildContext context) {
if (_loading) return const CircularProgressIndicator();
// ⚠️ süzgəc hər rebuild-də yenidən hesablanır
final visible = _orders.where((o) => switch (_filter) {
OrderFilter.all => true,
OrderFilter.active => o.status.isActive,
OrderFilter.completed => o.status == OrderStatus.completed,
}).toList();
// ⚠️ biznes qaydası widget-də
final total = visible.fold<double>(0, (s, o) => s + o.total) * 1.18;
return Column(children: [
Text('Cəmi (ƏDV ilə): ${total.toStringAsFixed(2)}'),
Expanded(child: OrderListView(orders: visible)),
]);
}
}
// ══════ ✅ TƏMİZ WIDGET ══════
class OrdersPage extends ConsumerWidget {
const OrdersPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(orderListNotifierProvider);
final visible = ref.watch(visibleOrdersProvider); // törəmə provider
final summary = ref.watch(orderSummaryProvider); // ƏDV domain-də
return Scaffold(
appBar: AppBar(
title: Text(AppLocalizations.of(context).ordersTitle),
bottom: FilterTabs(
// Sadə callback — icazəli
onChanged: (f) => ref
.read(orderListNotifierProvider.notifier)
.setFilter(f),
),
),
// Cihaz məlumatına görə layout — icazəli istisna
body: LayoutBuilder(
builder: (context, constraints) => switch (state) {
AsyncData() when visible.isEmpty => const EmptyOrders(),
AsyncData() => constraints.maxWidth > 600
? OrdersWideLayout(orders: visible, summary: summary)
: OrdersNarrowLayout(orders: visible, summary: summary),
AsyncError(:final error) => ErrorView(
error: error,
onRetry: () => ref.invalidate(orderListNotifierProvider),
),
_ => const Center(child: CircularProgressIndicator()),
},
),
);
}
}
// Diqqət: `build`-də nə `await`, nə `setState`, nə `where`, nə `* 1.18`.
// Formatlaşdırma isə widget-də qalır — o, göstərmə məntiqidir:
// Text(formatMoney(summary.total, Localizations.localeOf(context)))Eyni ekran: məntiqli widget və təmiz widget. İkincidə `build` yalnız göstərir.
`StatefulWidget` hələ də lazımdır. "Bütün state notifier-də olsun" qaydası çox uzağa aparıla bilər. Widget-ə aid obyektlərin yeri `StatefulWidget`-dir:
TextEditingController,FocusNode,ScrollController,PageControllerAnimationControllervəTickerProviderGlobalKey,FormState
Səbəb sadədir: bu obyektlərin yaradılması və `dispose` edilməsi widget-in ömrünə bağlıdır. Onları notifier-də saxlamaq iki problem yaradır: notifier dispose olunduqda controller sızır, ya da controller yaşayarkən widget yenidən qurulduqda köhnə obyektə istinad qalır.
Praktik bölgü nümunəsi (axtarış sahəsi):
TextEditingController→StatefulWidget-də.- Axtarış mətninin dəyəri və nəticələr → notifier-də.
- Controller-in
onChanged-i notifier-in metodunu çağırır.
Widget-in ölçüsü barədə. Rəsmi tövsiyələr adlandırma bölməsində ui/core/ kimi qovluqları məsləhət bilir; praktikada isə widget-i bölmək üçün sadə meyar var: build metodu ekranda görünmədən oxunmursa (təxminən 60-80 sətirdən çox), onu alt widget-lərə bölmək lazımdır. Alt widget-lər həm oxunaqlığı, həm rebuild dairəsini yaxşılaşdırır — const konstruktorlu ayrı widget ata rebuild olduqda yenidən qurulmur.
Praktika. Layihəndən bir ekran seç və build metodunu audit et. Hər sətir üçün sual ver: bu, rəsmi icazə verilən dörd istisnadan birinə düşürmü (sadə if, animasiya, cihaza görə layout, sadə routing)?
Düşməyən hər sətri köçür: məlumat məntiqi → notifier ya da törəmə provider; biznes qaydası → domain; xəta qərarı → presentFailure funksiyası.
Hazır sayılır: build-də await, setState, where/fold/sort və birbaşa repository çağırışı yoxdur; TextEditingController isə hələ də StatefulWidget-də qalır (bu, düzgündür).
📚 Mənbələr və sənədlər
- Arxitektura tövsiyələri: widget-də məntiqrəsmidocs.flutter.dev
Widget-də qala bilən dörd istisnanın rəsmi siyahısı — bu mövzunun mənbəyi.
- Case study: UI qatırəsmidocs.flutter.dev
View-un real kodda necə görünməsi və view model ilə əlaqəsi.
- Flutter: performansın yaxşılaşdırılmasırəsmidocs.flutter.dev
`build`-də ağır iş görməmək və rebuild dairəsini daraltmaq — widget bölgüsünün ikinci səbəbi.
- Flutter: StatefulWidgetrəsmiapi.flutter.dev
Controller-lərin `initState`/`dispose` ilə idarəsi — niyə onlar notifier-də saxlanmır.