# 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:
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ę:
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:
- 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
Brak komentarzy.