Reference location:
.claude/references/patterns/usecase-patterns.md
UseCase is a core component of Clean Architecture that encapsulates business logic.
Standard Pattern#
| Pattern | Description |
|---|---|
| Optional Constructor Injection | â Standard pattern - Easy mock injection, no getIt dependency |
Future UseCase (Single Request)#
UseCase Definition#
// domain/usecase/get_user_usecase.dart
class GetUserUseCase {
/// [GetUserUseCase]뼟 ěěąíŠëë¤.
const GetUserUseCase([IUserRepository? repository])
: _repo = repository ?? const UserRepository();
final IUserRepository _repo;
Future<Either<Failure, User>> call(GetUserParams params) async {
return _repo.getUser(params.id);
}
}
class GetUserParams {
const GetUserParams({required this.id});
final int id;
}
Usage in BLoC (DI Decoupling)#
class UserBloc extends Bloc<UserEvent, UserState> {
UserBloc({
GetUserUseCase? getUserUseCase,
}) : _getUserUseCase = getUserUseCase ?? const GetUserUseCase(),
super(const UserInitial()) {
on<UserLoad>(_onLoad);
}
final GetUserUseCase _getUserUseCase;
Future<void> _onLoad(UserLoad event, Emitter<UserState> emit) async {
emit(const UserLoading());
final result = await _getUserUseCase(
GetUserParams(id: event.userId),
);
if (isClosed) return; // await í ě˛´íŹ íě!
result.fold(
(failure) => emit(UserError(failure: failure)),
(user) => emit(UserLoaded(user: user)),
);
}
}
Tests#
class MockGetUserUseCase extends Mock implements GetUserUseCase {}
blocTest<UserBloc, UserState>(
'emits [loading, loaded] when load succeeds',
setUp: () {
when(() => mockGetUser(any()))
.thenAnswer((_) async => right(testUser));
},
build: () => UserBloc(getUserUseCase: mockGetUser),
act: (bloc) => bloc.add(const UserLoad(userId: 1)),
expect: () => [
const UserLoading(),
UserLoaded(user: testUser),
],
);
Stream UseCase (SWR/Cache-First)#
A Stream-based UseCase that sequentially emits cached data and server data.
StreamUseCase Interface#
/// Stream ę¸°ë° UseCase (SWR í¨í´ěŠ)
abstract class StreamUseCase<T, Params, Repo> {
Repo get repo;
Stream<Either<Failure, T>> call(Params param);
}
StreamUseCase Implementation#
class GetItemsStreamUseCase
implements StreamUseCase<List<Item>, GetItemsParams, IItemRepository> {
const GetItemsStreamUseCase([IItemRepository? repository])
: _repository = repository ?? const ItemRepository();
final IItemRepository _repository;
@override
IItemRepository get repo => _repository;
@override
Stream<Either<Failure, List<Item>>> call(GetItemsParams params) {
return repo.getItemsWithCache(categoryId: params.categoryId);
}
}
Usage in BLoC: emit.forEach + restartable()#
import 'package:bloc_concurrency/bloc_concurrency.dart';
class ItemBloc extends Bloc<ItemEvent, ItemState> {
ItemBloc({
GetItemsStreamUseCase? getItemsStream,
}) : _getItemsStream = getItemsStream ?? const GetItemsStreamUseCase(),
super(const ItemState()) {
on<_Load>(
_onLoad,
transformer: restartable(), // íëźëŻ¸í° ëłę˛˝ ě ě´ě ě¤í¸ëŚź ěë 졨ě
);
}
final GetItemsStreamUseCase _getItemsStream;
Future<void> _onLoad(_Load event, Emitter<ItemState> emit) async {
if (state.items.isEmpty) {
emit(state.copyWith(status: LoadingStatus.loading));
}
await emit.forEach<Either<Failure, List<Item>>>(
_getItemsStream(GetItemsParams(categoryId: event.categoryId)),
onData: (result) => result.fold(
(failure) => state.copyWith(
status: LoadingStatus.error,
failure: failure,
),
(items) => state.copyWith(
status: LoadingStatus.loaded,
items: items,
lastUpdated: DateTime.now(),
),
),
);
}
}
How restartable() Works
1. add(_Load(categoryId: 1)) â ě¤í¸ëŚźA 꾏ë
(ěşě â ěë˛ ěě°¨ yield)
2. add(_Load(categoryId: 2)) â restartable()ę° í¸ë¤ëŹ1 졨ě â ě¤í¸ëŚźA í´ě
â í¸ë¤ëŹ2 ěě â ě¤í¸ëŚźB 꾏ë
emit.forEach internally calls stream.listen(), and when restartable()
cancels the handler, the subscription is automatically cleaned up. No need to override close().
Repository SWR Stream Implementation#
@override
Stream<Either<Failure, List<Item>>> getItemsWithCache({
required int categoryId,
}) async* {
// 1. ěşě ë°ě´í° ěŚě yield
final cached = await _itemDao.getByCategoryId(categoryId);
if (cached != null && cached.isNotEmpty) {
yield right(cached.map((e) => e.toDomain()).toList());
}
// 2. ěë˛ ë°ě´í° fetch â yield
try {
final remote = await _client.item.getItems(categoryId: categoryId);
await _itemDao.upsertAll(remote.map((e) => e.toLocal()).toList());
yield right(remote);
} on Exception catch (error, stackTrace) {
if (cached == null || cached.isEmpty) {
yield left(ItemFailure(message: error.toString()));
}
// Ignore error if cache existed (cached data already displayed)
Log.e('ěë˛ ę°ąě ě¤í¨', error: error, stackTrace: stackTrace);
}
}
Stream Test#
class MockGetItemsStreamUseCase extends Mock implements GetItemsStreamUseCase {}
blocTest<ItemBloc, ItemState>(
'emits cached then fresh data via SWR stream',
setUp: () {
when(() => mockGetItemsStream(any())).thenAnswer(
(_) => Stream.fromIterable([
right(cachedItems),
right(freshItems),
]),
);
},
build: () => ItemBloc(getItemsStream: mockGetItemsStream),
act: (bloc) => bloc.add(const ItemEvent.load(categoryId: 1)),
expect: () => [
ItemState(status: LoadingStatus.loading),
ItemState(status: LoadingStatus.loaded, items: cachedItems),
ItemState(status: LoadingStatus.loaded, items: freshItems),
],
);
UseCase Bundle (8 or more)#
When constructor parameters become numerous, group them into a Bundle class.
class DashboardUseCases {
const DashboardUseCases({
GetStatsUseCase? getStats,
GetChartDataUseCase? getChartData,
GetRecentItemsUseCase? getRecentItems,
}) : getStats = getStats ?? const GetStatsUseCase(),
getChartData = getChartData ?? const GetChartDataUseCase(),
getRecentItems = getRecentItems ?? const GetRecentItemsUseCase();
final GetStatsUseCase getStats;
final GetChartDataUseCase getChartData;
final GetRecentItemsUseCase getRecentItems;
}
class DashboardBloc extends Bloc<DashboardEvent, DashboardState> {
DashboardBloc({DashboardUseCases? useCases})
: _useCases = useCases ?? const DashboardUseCases(),
super(const DashboardState()) {
on<_Load>(_onLoad);
}
final DashboardUseCases _useCases;
}
Selection Guide#
ë°ě´í° íëŚ?
âââ ë¨ęą´ ěě˛ â Future UseCase
âââ ěşě+ěë˛ ěě°¨ â Stream UseCase + emit.forEach + restartable()
UseCase injection â Optional Constructor Injection (standard)
UseCase ę°ě?
âââ 1~7ę° â ę°ëł ěěąě íëźëݏí°
âââ 8ę°+ â UseCase Bundle í¨í´
Referencing Agents#
/feature:domain- Domain Layer Generation/feature:bloc- BLoC state management/feature:presentation- Presentation Layer generation