Domain modelləri və immutability (freezed)
Domain modeli — tətbiqin öz lüğətidir. O, backend-in JSON-una, bazanın cədvəlinə ya da ekranın formasına yox, biznesin özünə uyğun gəlir.
Domain modelinin üç xüsusiyyəti var:
- Immutable — yaradıldıqdan sonra dəyişmir. Dəyişiklik lazımdırsa,
copyWithyeni obyekt qaytarır. - Dəyər bərabərliyi (value equality) — iki obyektin sahələri eynidirsə, onlar bərabərdir. Bu, referens bərabərliyindən fərqlidir və rebuild-lərə birbaşa təsir edir.
- Yalnız biznes mənası —
isExpensive,finalPrice,canBeCancelledkimi.Color,TextStyle,TimeOfDayburada olmur.
Rəsmi Flutter tövsiyələri immutable modelləri ən yüksək prioritetli bənd sayır: səbəb dəyişikliyin öz yerində baş verməsinə zəmanət verməkdir. Mutable model UI qatına məlumatı təsadüfən dəyişmək imkanı verir və tək istiqamətli axını sındırır.
// fayl: lib/domain/models/product.dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'product.freezed.dart';
@freezed
abstract class Product with _$Product {
// Private boş konstruktor: generasiya olunan kodun sinfi extend etməsinə
// imkan verir — öz getter/metodlarını yazmaq üçün TƏLƏB OLUNUR.
const Product._();
const factory Product({
required String id,
required String title,
required double price, // artıq manatla, çevirmə mapper-də olub
@Default(0) int discountPercent,
@Default(false) bool isArchived,
DateTime? createdAt,
}) = _Product;
// Biznes qaydaları modelin özündə yaşayır — hər ekranda təkrarlanmır.
double get finalPrice => price * (100 - discountPercent) / 100;
bool get hasDiscount => discountPercent > 0;
bool get isPurchasable => !isArchived && price > 0;
}
// İstifadə:
const tea = Product(id: 'p1', title: 'Çay', price: 10, discountPercent: 10);
tea.finalPrice; // 9.0
tea.copyWith(discountPercent: 0); // yeni obyekt, köhnəsi dəyişmir
// Dəyər bərabərliyi: freezed `==` və `hashCode`-u generasiya edir.
const same = Product(id: 'p1', title: 'Çay', price: 10, discountPercent: 10);
assert(tea == same); // true — sahələr eynidir
// Generasiya: dart run build_runner build --delete-conflicting-outputsfreezed ilə domain modeli. `const Product._();` sətri vacibdir — o olmadan öz getter və metodlarını əlavə edə bilmirsən.
| Yanaşma | `copyWith` | `==` / `hashCode` | JSON | Qiyməti |
|---|---|---|---|---|
| Əl ilə (`final` sahələr) | Özün yazırsan | Özün override edirsən | Özün yazırsan | Kod generasiyası yoxdur, lakin 5 sahəli modeldə ~40 sətir boilerplate və sahə əlavə edərkən unutma riski |
| `Equatable` | Özün yazırsan | `props` siyahısı ilə hazırdır | Özün yazırsan | Kod generasiyası yoxdur; `props`-a yeni sahəni əlavə etməyi unutmaq səssiz baq yaradır |
| `freezed` | Generasiya olunur | Generasiya olunur | `json_serializable` ilə birlikdə generasiya olunur | `build_runner` lazımdır və böyük layihədə build vaxtı artır; rəsmi tövsiyə bunu açıq qeyd edir |
Dəyər bərabərliyinin Riverpod ilə birlikdə çox konkret nəticəsi var: Riverpod 3-də bütün `updateShouldNotify` implementasiyaları `==` müqayisəsindən istifadə edir.
Bu, praktikada nə deməkdir:
- Model
==override etməyibsə (adi sinif, referens bərabərliyi), hər yeni obyekt "dəyişmiş" sayılır və dinləyicilər yenidən qurulur — hətta sahələr eyni olduqda. - Model dəyər bərabərliyinə malikdirsə, repository eyni məlumatı ikinci dəfə qaytardıqda rebuild olmur.
Yəni immutability + == təkcə "təmiz kod" məsələsi deyil, ölçülə bilən performans qərarıdır. Siyahı modellərində bu, xüsusilə hiss olunur: List<Product> müqayisəsi elementlərin ==-inə söykənir.
Seçim tövsiyəsi: JSON lazım olan yerdə freezed (DTO-lar), domain modellərində freezed ya da Equatable — layihədə artıq build_runner varsa, hər ikisində freezed işlətmək daha az qərar deməkdir.
Bir vacib nüans: freezed_annotation təmiz Dart paketidir — asılılıqları collection, json_annotation və meta-dır, Flutter-dən asılı deyil. Ona görə onu domain qatında import etmək asılılıq qaydasını pozmur. Kod generatoru (freezed, build_runner) isə dev_dependencies-də qalır və son build-ə düşmür.
Praktika. Layihəndə bir domain modeli seç və onu freezed-ə çevir: copyWith, == və bir biznes getter-i (finalPrice kimi) əlavə et. Sonra o getter üçün bir unit test yaz.
Hazır sayılır: dart run build_runner build xətasız işləyir, test yaşıldır, və həmin hesablama artıq heç bir widget faylında təkrarlanmır.
📚 Mənbələr və sənədlər
- freezed paketirəsmipub.dev
Sintaksisin mənbəyi: `abstract`/`sealed` tələbi, `const X._()` qaydası, `@Default`, JSON integrasiyası.
- freezed_annotationrəsmipub.dev
Domain qatında import etmək üçün vacib fakt: bu paketin Flutter asılılığı yoxdur.
- Dart: bərabərlik və `hashCode`rəsmidart.dev
Dart-ın dizayn tövsiyələri — `==` və `hashCode`-un birlikdə override edilməsi qaydası da burada.
- Arxitektura tövsiyələri: immutable modellərrəsmidocs.flutter.dev
Immutable modellərin niyə ən yüksək prioritetli tövsiyə olduğu və `freezed`/`built_value` mövqeyi.