Powrót do strony głównej

Use-cases w BLoC Flutter: refaktoryzacja architektury

Artykuł opisuje refaktoryzację BLoC w Flutter poprzez wydzielenie logiki biznesowej do use-cases. Podane są przykłady kodu przed i po, zalety dla testowania i skalowalności. Podejście oparte na czystej architekturze.

BLoC + use-cases: jak uprościć architekturę Flutter
Advertisement 728x90

# Rozdzielność BLoC i use-cases w Flutter: czysta architektura w praktyce

BLoC w Flutter często przeradza się w god object: obsługa zdarzeń miesza wywołania serwisów, walidację i emisję stanów. To komplikuje utrzymanie, testowanie i skalowanie. Wyodrębnienie logiki biznesowej do use-cases przywraca separację warstw zgodnie z zasadami czystej architektury: presentation (UI i BLoC), domain (use-cases, encje) i data (repozytoria, serwisy).

Use-case realizuje konkretny scenariusz: orkiestruje serwisy, obsługuje błędy, stosuje reguły biznesowe, nie zależąc od UI. BLoC skupia się na unidirectional flow — przyjmuje zdarzenia i emituje stany.

Klasyczny BLoC: gdzie logika miesza się z UI

Typowy BLoC bezpośrednio wywołuje serwisy przez DI, wykonując side-effects w handlerach. Przykład uproszczonego ItemsBloc:

Google AdInline article slot
class ItemsBloc extends Bloc<ItemsEvent, ItemsState> {
  final FetchItemsService _service;

  ItemsBloc(this._service) : super(Initial()) {
    on<FetchItemsEvent>(_onFetchItemsEvent);
  }

  Future<void> _onFetchItemsEvent(FetchItemsEvent event, Emitter<ItemsState> emit) async {
    emit(Loading());
    try {
      final items = await _service.fetchItems();
      emit(Loaded(items: items));
    } catch (error) {
      emit(Error(error: error));
    }
  }
}

Problemy: wysoka sprzężoność, trudne testowanie, naruszenie SRP. BLoC zna dane, sieć i logikę biznesową.

Refaktoring: BLoC jako czysty state manager

Po refaktoringu BLoC zawiera tylko mapowanie zdarzeń na stany, bez zależności:

class ItemsBloc extends Bloc<ItemsEvent, ItemsState> {
  ItemsBloc() : super(Initial()) {
    on<SetLoading>(_onSetLoading);
    on<SetLoaded>(_onSetLoaded);
    on<SetError>(_onSetError);
  }

  Future<void> _onSetLoading(SetLoading event, Emitter<ItemsState> emit) async {
    emit(Loading());
  }

  Future<void> _onSetLoaded(SetLoaded event, Emitter<ItemsState> emit) async {
    emit(Loaded(items: event.items));
  }

  Future<void> _onSetError(SetError event, Emitter<ItemsState> emit) async {
    emit(Error(error: event.error));
  }
}

Use-case przejmuje orkiestrację:

Google AdInline article slot
class FetchItemsUseCase {
  final ItemsBloc bloc;
  final FetchItemsService service;

  FetchItemsUseCase({required this.bloc, required this.service});

  Future<void> call() async {
    bloc.add(SetLoading());
    try {
      final items = await service.fetchItems();
      bloc.add(SetLoaded(items: items));
    } catch (error) {
      bloc.add(SetError(error: error.toString()));
    }
  }
}

BLoC skraca się do 20 linii, staje się testowalny: mock zdarzeń i sprawdzenie stanów.

Zalety use-cases jako orkiestratora

Use-case to nie zwykły proxy — łączy dane z repozytoriów, stosuje reguły, loguje, synchronizuje. Zależy od abstrakcji (interfejsów), nie od implementacji.

Kluczowe zalety:

Google AdInline article slot
  • Testowalność: mock serwisów/repozytoriów do testów jednostkowych use-case.
  • Skalowalność: nowe scenariusze dodawane bez zmian w BLoC (Open/Closed).
  • Elastyczność DI: serwisy wstrzykiwane do use-case, BLoC niezależny.
  • Czytelność: scenariusze biznesowe izolowane w osobnych klasach.

W realnym projekcie refaktoring skrócił czas development o 30%, zmniejszył błędy, uprościł testy.

Przepływ danych i kompromisy

UI → use-case → serwisy/repozytoria → zdarzenia do BLoC → stan UI. Use-case zna BLoC (kompromis czystej architektury dla prostoty), ale UI jest oczyszczone z bloc.add.

BLoC to unidirectional data flow: przewidywalne zachowanie, łatwe testy. Use-cases testowane osobno: symulacja odpowiedzi, sprawdzenie wywołań.

Co najważniejsze

  • Separacja warstw: BLoC — state manager, use-cases — logika domenowa.
  • SRP w praktyce: każda klasa ma jedną odpowiedzialność.
  • Uproszczone testowanie: czyste funkcje w BLoC, mocki w use-cases.
  • Skala: podejście działa z Cubit, Riverpod czy innymi state managerami.

Podejście nadaje się do wszystkich projektów Flutter, gdzie BLoC rośnie. Zmierz metryki przed/po: rozmiar klas, coverage testów, czas na nowe funkcje.

— Editorial Team

Advertisement 728x90

Czytaj dalej