Sparround

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: Future gözləmək, nəticəni saxlamaq.
KodWidget-də?Səbəb
`if (state.isAdmin) AdminPanel()`✅ OlarBayraq view model-dən gəlir; widget yalnız göstərir
`if (constraints.maxWidth > 600) Row() else Column()`✅ OlarCihaz məlumatına görə layout — icazəli istisna
`AnimationController` və `Tween` hesablamaları✅ OlarAnimasiya widget-in ömrünə bağlıdır
`orders.where((o) => o.status.isActive).toList()`❌ OlmazMəlumat məntiqidir; hər rebuild-də yenidən hesablanır və test edilə bilmir
`if (error is NetworkFailure) showRetry()`⚠️ SərhədQərar presentation qatına aiddir, lakin `presentFailure(...)` kimi ayrı funksiyada saxlanmalıdır
`await repository.fetchMine()`❌ OlmazData qatına birbaşa müraciət; view model tamamilə keçilir
`total * 1.18` (ƏDV hesablanması)❌ OlmazBiznes qaydası — domain modelinə ya da use-case-ə aiddir
`NumberFormat.currency(...).format(total)`✅ OlarGöstərmə formatıdır; lokala bağlıdır və UI qatına aiddir
dart
// ══════ ❌ 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, PageController
  • AnimationControllerTickerProvider
  • GlobalKey, 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):

  • TextEditingControllerStatefulWidget-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