Sparround

State modelləşdirmə: status enum vs sealed class

Rəsmi bloc sənədi state modelləşdirməyə ayrı bölmə həsr edir və iki yanaşma göstərir; hər ikisi tövsiyədir, məcburiyyət deyil.

1. Tək konkret sinif + status enum. Bütün vəziyyətlər üçün bir sinif; enum cari statusu göstərir; sahələr nullable olur və statusa görə şərh edilir. Sənədin qeydi: bu yanaşma vəziyyətlər bir-birini tam istisna etmədikdə və çoxlu paylaşılan sahə olduqda daha yaxşı işləyir.

2. Sealed sinif + alt siniflər. Hər vəziyyət ayrı tipdir. Vəziyyətlər bir-birini istisna edirsə uyğundur; Dart 3-ün pattern matching-i ilə kompilyator bütün halların əhatə olunduğunu yoxlayır.

Sənəd hər iki halda praktik əlavələri də sadalayır: package:equatable-dan Equatable genişləndirmək, @immutable annotasiyası, copyWith metodu və mümkün olduqda const konstruktor.

dart
enum TodoStatus { initial, loading, success, failure }

final class TodoState extends Equatable {
  const TodoState({
    this.status = TodoStatus.initial,
    this.todos = const [],
    this.exception,
  });

  final TodoStatus status;
  final List<Todo> todos;
  final Exception? exception;

  TodoState copyWith({
    TodoStatus? status,
    List<Todo>? todos,
    Exception? exception,
  }) {
    return TodoState(
      status: status ?? this.status,
      todos: todos ?? this.todos,
      exception: exception ?? this.exception,
    );
  }

  // Yeni sahə əlavə edildikdə props-a da əlavə etmək YADDAN ÇIXMAMALIDIR:
  // əks halda == iki fərqli state-i bərabər sayır və UI yenilənmir.
  @override
  List<Object?> get props => [status, todos, exception];
}

Yanaşma 1: status enum — sahələr paylaşılır.

dart
sealed class TodoState {
  const TodoState();
}

final class TodoInitial extends TodoState {
  const TodoInitial();
}

final class TodoLoadInProgress extends TodoState {
  const TodoLoadInProgress();
}

final class TodoLoadSuccess extends TodoState {
  const TodoLoadSuccess(this.todos);
  final List<Todo> todos;
}

final class TodoLoadFailure extends TodoState {
  const TodoLoadFailure(this.message);
  final String message;
}

// UI: kompilyator bütün halların əhatə olunduğunu yoxlayır.
// Yeni state sinfi əlavə edildikdə bu switch kompilyasiya olunmur —
// səhv runtime yerinə compile-time-da görünür.
Widget build(BuildContext context) {
  return BlocBuilder<TodoBloc, TodoState>(
    builder: (context, state) => switch (state) {
      TodoInitial() => const SizedBox.shrink(),
      TodoLoadInProgress() => const TodoSkeleton(),
      TodoLoadSuccess(:final todos) => TodoListView(todos: todos),
      TodoLoadFailure(:final message) => ErrorView(message: message),
    },
  );
}

Yanaşma 2: sealed siniflər — vəziyyətlər bir-birini istisna edir.

KriteriyaStatus enumSealed siniflər
Vəziyyətlər bir-birini istisna edirZəif uyğun — nullable sahələr yığılırTəbii uyğun
Çoxlu paylaşılan sahə (filtr, səhifə, sorğu)Rahat — `copyWith` bir yerdəTəkrarlanma yaranır
Yükləmə zamanı köhnə məlumatı göstərməkAsan: `status: loading` + mövcud `todos`Əlavə iş: state-ə köhnə dəyəri daşımaq lazımdır
Kompilyator yoxlamasıYoxdur — `switch` üzərində `default` qalırVar — bütün hallar məcburidir
Mövcud koda tədricən əlavə etməkAsandırRefactoring tələb edir

Equatable ilə ən çox rastlanan bug: state sinfinə yeni sahə əlavə olunur, lakin props siyahısına yazılmır. Nəticədə iki fərqli state == üzrə bərabər sayılır, bloc təkrar state qaydasına görə dəyişikliyi buraxmır və UI heç vaxt yenilənmir. Bu səhvin qarşısını almaq üçün ya freezed kimi kod generasiyası (props əl ilə yazılmır), ya da state siniflərini örtən test istifadə olunur.

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