Sparround

Flutter-in rəsmi arxitekturası: UI və data qatları

Flutter komandasının rəsmi arxitektura bələdçisi iki əsas qat verir və üçüncüsünü opsional sayır.

UI qatı

  • View — widget kompozisiyası. Burada yalnız göstərmə məntiqi qalır.
  • View model — repository-dən gələn məlumatı ekran üçün state-ə çevirir, view rebuild olarkən state-i saxlayır və view-a callback-lər (command-lar) açır.

Data qatı

  • Repository — model məlumatı üçün vahid həqiqət mənbəyi: cache, təkrar cəhd, xəta idarəsi, xam məlumatın domain modelinə çevrilməsi.
  • Service — xarici API-ı (REST, platforma, fayl sistemi, baza) örtür və state saxlamır.

Domain qatı (opsional) — use-case/interactor-lar. Bələdçi onu yalnız view model-lər çox mürəkkəbləşdikdə tövsiyə edir.

Bələdçi bu quruluşu MVVM ilə eyniləşdirir. Vacib nüans: konkret state management library-si tələb olunmur — prinsiplər Provider, Riverpod və BLoC ilə eyni dərəcədə işləyir.

Rəsmi tövsiyəPrioritetPraktik oxunuşu
Data və UI qatlarını aydın ayırmaqƏn yüksəkBu, siyahının təməlidir — separation of concerns ən vacib prinsip adlandırılır
Data qatında repository pattern-iƏn yüksəkHər məlumat növü üçün bir repository; UI birbaşa API-yə çıxmır
UI qatında view + view model (MVVM)Ən yüksəkWidget "axmaq" qalır, UI məntiqi ayrı sinifdə test olunur
Widget-də məntiq saxlamamaqƏn yüksəkİcazə verilən istisnalar: göstər/gizlət şərti, animasiya, cihaza görə layout, sadə routing
Tək istiqamətli məlumat axınıƏn yüksəkMəlumat data → UI istiqamətində axır; istifadəçi hərəkətləri əks istiqamətdə hadisə kimi gedir
Immutable modellərƏn yüksəkUI qatı məlumatı təsadüfən dəyişə bilmir; dəyişiklik yalnız öz yerində baş verir
Dependency injectionƏn yüksəkQlobal obyektlərdən qaçmaq; bələdçi `provider` paketini nümunə göstərir
Abstract repository sinifləriƏn yüksəkMüxtəlif mühit üçün müxtəlif implementasiya (remote, local, fake)
Test üçün fake-lər yazmaqƏn yüksəkFake giriş/çıxışa fokuslanır və interfeysi sadə saxlamağa məcbur edir
Command pattern-iOrtaUI-dan gələn hadisələri standartlaşdırır, ikiqat basılmanı və render xətalarını azaldır
`freezed` və ya `built_value`OrtaImmutability, `copyWith`, `==` və JSON — kod generasiyası ilə; böyük layihədə build vaxtı artır
Domain qatı (use-case-lər)ŞərtliYalnız view model-ləri sıxışdıran mürəkkəb məntiq olduqda; əksər tətbiqdə artıq yükdür
Ayrı API və domain modelləriŞərtliBöyük tətbiqlərdə tövsiyə olunur; kiçikdə əlavə söz-söhbət yaradır

Rəsmi bələdçinin nümunələri ChangeNotifierpackage:provider üzərində yazılıb. Sənin stack-in Riverpod-dursa, uyğunluq bir-birədir — dəyişən yalnız sintaksisdir:

  • View modelNotifier / AsyncNotifier (kod generasiyası ilə @riverpod class ... extends _$...).
  • Dependency injection → provider-lərin özü. Repository və service adi Dart sinifləridir; onları qaytaran provider-lər asılılıq qrafını qurur.
  • Command → notifier-in metodu (refresh(), submit()), view isə yalnız onu çağırır.
  • Loading / error stateAsyncValue (Riverpod 3-də sealed, yəni switch tam yoxlanılır).

Bir vacib fərq: rəsmi bələdçidə view model BuildContext vasitəsilə tapılır, Riverpod-da isə ref vasitəsilə. Praktik nəticə — notifier-in testi widget qaldırmadan mümkündür.

dart
// ══ 1. data/services/product_api_client.dart ══ (state saxlamır)
class ProductApiClient {
  ProductApiClient({required http.Client client}) : _client = client;
  final http.Client _client;

  Future<List<ProductDto>> getActiveProducts() async {
    final response = await _client.get(
      Uri.parse('https://api.example.com/v1/products?active=true'),
    );
    if (response.statusCode != 200) {
      throw HttpException('status ${response.statusCode}');
    }
    final list = jsonDecode(response.body)['data'] as List<dynamic>;
    return list
        .map((e) => ProductDto.fromJson(e as Map<String, dynamic>))
        .toList();
  }
}

// ══ 2. data/repositories/product_repository_remote.dart ══ (həqiqətin mənbəyi)
class ProductRepositoryRemote implements ProductRepository {
  ProductRepositoryRemote({required ProductApiClient apiClient})
      : _apiClient = apiClient;
  final ProductApiClient _apiClient;

  @override
  Future<List<Product>> fetchActive() async {
    final dtos = await _apiClient.getActiveProducts();
    return dtos.map((dto) => dto.toDomain()).toList();  // DTO → domain
  }
}

// ══ 3. presentation/products/product_list_notifier.dart ══ (view model)
part 'product_list_notifier.g.dart';

@riverpod
ProductApiClient productApiClient(Ref ref) =>
    ProductApiClient(client: http.Client());

@riverpod
ProductRepository productRepository(Ref ref) =>
    ProductRepositoryRemote(apiClient: ref.watch(productApiClientProvider));

@riverpod
class ProductList extends _$ProductList {
  @override
  Future<List<Product>> build() =>
      ref.watch(productRepositoryProvider).fetchActive();

  // "Command": view yalnız bunu çağırır, necə işlədiyini bilmir.
  Future<void> refresh() => ref.refresh(productListProvider.future);
}

// ══ 4. presentation/products/products_page.dart ══ (view)
class ProductsPage extends ConsumerWidget {
  const ProductsPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(productListProvider);

    // AsyncValue Riverpod 3-də sealed-dir: switch tam yoxlanılır.
    return switch (state) {
      AsyncData(:final value) => RefreshIndicator(
          onRefresh: () =>
              ref.read(productListProvider.notifier).refresh(),
          child: ListView.builder(
            itemCount: value.length,
            itemBuilder: (_, i) => ProductTile(product: value[i]),
          ),
        ),
      AsyncError(:final error) => ErrorView(error: error),
      _ => const Center(child: CircularProgressIndicator()),
    };
  }
}

Bir feature, dörd fayl: service → repository → notifier → view. Hər fayl yalnız bir qatın işini görür.

Praktika. Yuxarıdaki dörd faylı öz layihəndə (ya da yeni flutter create layihəsində) real bir açıq API üzərində qur — məsələn pulsuz bir REST endpoint. ProductDto-nu hələlik əl ilə yaz (fromJson), freezed sonraki mərhələdədir.

Hazır sayılır: ekran məlumatı göstərir, products_page.dart faylında http, jsonDecode və heç bir JSON açarı görünmür, RefreshIndicator işləyir.

📚 Mənbələr və sənədlər

  • Flutter arxitektura bələdçisirəsmidocs.flutter.dev

    View, view model, repository, service və opsional domain qatının rəsmi təsviri.

  • Arxitektura tövsiyələrirəsmidocs.flutter.dev

    Yuxarıdaki cədvəlin mənbəyi — hər tövsiyənin prioriteti ilə birlikdə.

  • Case study: UI qatırəsmidocs.flutter.dev

    View və view model-in real kodda necə göründüyü, `ChangeNotifier` nümunəsi ilə.

  • Riverpod: başlanğıcrəsmiriverpod.dev

    `flutter_riverpod`, `riverpod_annotation`, `riverpod_generator` quraşdırılması və `@riverpod` sintaksisi.