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