Use Cases in Juice
Use cases are the building blocks of business logic in Juice. Each use case represents a single operation that can be performed in your application.
Basic Use Case Pattern
Creating a Use Case
Every use case extends BlocUseCase
with the bloc type and event type:
class SendMessageUseCase extends BlocUseCase<ChatBloc, SendMessageEvent> {
@override
Future<void> execute(SendMessageEvent event) async {
emitWaiting(groupsToRebuild: {"chat_status"});
try {
await chatService.send(event.message);
emitUpdate(
newState: bloc.state.copyWith(
messages: [...bloc.state.messages, event.message]
),
groupsToRebuild: {"chat_messages", "chat_status"}
);
} catch (e) {
emitFailure(groupsToRebuild: {"chat_status"});
}
}
}
Registering in the Bloc
Use cases are registered in the bloc constructor using UseCaseBuilder
:
class ChatBloc extends JuiceBloc<ChatState> {
ChatBloc(this._chatService) : super(
ChatState.initial(),
[
// Basic registration
() => UseCaseBuilder(
typeOfEvent: SendMessageEvent,
useCaseGenerator: () => SendMessageUseCase(),
),
// With initial event
() => UseCaseBuilder(
typeOfEvent: LoadMessagesEvent,
useCaseGenerator: () => LoadMessagesUseCase(),
initialEventBuilder: () => LoadMessagesEvent(),
),
],
[], // Aviators
);
final ChatService _chatService;
}
Basic Use Case Types
- Update Use Case - Simple state updates:
class UpdateProfileUseCase extends BlocUseCase<ProfileBloc, UpdateProfileEvent> { @override Future<void> execute(UpdateProfileEvent event) async { emitUpdate( newState: bloc.state.copyWith( name: event.name, email: event.email ), groupsToRebuild: {"profile"} ); } }
- Loading Use Case - Data fetching:
class LoadOrdersUseCase extends BlocUseCase<OrderBloc, LoadOrdersEvent> { @override Future<void> execute(LoadOrdersEvent event) async { emitWaiting(groupsToRebuild: {"orders_list"}); try { final orders = await orderService.fetchOrders(); emitUpdate( newState: bloc.state.copyWith(orders: orders), groupsToRebuild: {"orders_list", "orders_count"} ); } catch (e) { emitFailure(groupsToRebuild: {"orders_list"}); } } }
- Operation Use Case - Complex operations:
class ProcessPaymentUseCase extends BlocUseCase<PaymentBloc, ProcessPaymentEvent> { @override Future<void> execute(ProcessPaymentEvent event) async { emitWaiting(groupsToRebuild: {"payment_status"}); try { // Process payment final result = await paymentService.process(event.payment); // Update order status await orderService.updateStatus(event.orderId, OrderStatus.paid); // Navigate on success emitUpdate( newState: bloc.state.copyWith( paymentResult: result, orderStatus: OrderStatus.paid ), groupsToRebuild: {"payment_status", "order_status"}, aviatorName: 'orderComplete', aviatorArgs: {'orderId': event.orderId} ); } catch (e) { emitFailure( newState: bloc.state.copyWith(error: e.toString()), groupsToRebuild: {"payment_status"} ); } } }
Advanced Use Cases
Stateful Use Cases
When you need to maintain state across event handling:
class WebSocketUseCase extends StatefulUseCaseBuilder<ChatBloc, ConnectEvent> {
WebSocket? _socket;
StreamSubscription? _subscription;
@override
Future<void> execute(ConnectEvent event) async {
_socket = await WebSocket.connect(event.url);
_subscription = _socket?.listen(
(message) {
bloc.send(MessageReceivedEvent(message));
},
onError: (error) {
bloc.send(ConnectionErrorEvent(error));
}
);
}
@override
Future<void> close() async {
await _subscription?.cancel();
await _socket?.close();
super.close();
}
}
Relay Use Cases
For connecting multiple blocs:
class ProfileBloc extends JuiceBloc<ProfileState> {
ProfileBloc() : super(
ProfileState.initial(),
[
// Relay from auth bloc changes to profile updates
() => RelayUseCaseBuilder<AuthBloc, ProfileBloc>(
typeOfEvent: UpdateProfileEvent,
statusToEventTransformer: (status) {
if (status.state.isAuthenticated) {
return UpdateProfileEvent(userId: status.state.userId);
}
return ClearProfileEvent();
},
useCaseGenerator: () => UpdateProfileUseCase(),
),
],
[],
);
}
Cancellable Use Cases
For long-running operations that can be cancelled:
class UploadFileUseCase extends BlocUseCase<UploadBloc, UploadFileEvent> {
@override
Future<void> execute(UploadFileEvent event) async {
emitWaiting(groupsToRebuild: {"upload_status"});
try {
if (event is CancellableEvent && event.isCancelled) {
emitCancel(groupsToRebuild: {"upload_status"});
return;
}
await uploadService.upload(
event.file,
onProgress: (progress) {
// Check cancellation during upload
if (event is CancellableEvent && event.isCancelled) {
throw CancelledException();
}
emitUpdate(
newState: bloc.state.copyWith(progress: progress),
groupsToRebuild: {"upload_progress"}
);
}
);
emitUpdate(
newState: bloc.state.copyWith(isComplete: true),
groupsToRebuild: {"upload_status", "upload_progress"}
);
} on CancelledException {
emitCancel(groupsToRebuild: {"upload_status"});
} catch (e) {
emitFailure(groupsToRebuild: {"upload_status"});
}
}
}
Composite Use Cases
For coordinating multiple operations:
class CheckoutUseCase extends BlocUseCase<CheckoutBloc, CheckoutEvent> {
@override
Future<void> execute(CheckoutEvent event) async {
emitWaiting(groupsToRebuild: {"checkout_status"});
try {
// Validate cart
final cart = await validateCart();
if (!cart.isValid) {
throw ValidationException('Invalid cart');
}
// Process payment
final payment = await processPayment(event.paymentDetails);
if (!payment.isSuccessful) {
throw PaymentException('Payment failed');
}
// Create order
final order = await createOrder(cart, payment);
// Send confirmation
await sendConfirmation(order);
// Update state and navigate
emitUpdate(
newState: bloc.state.copyWith(
order: order,
status: CheckoutStatus.complete
),
groupsToRebuild: {"checkout_status", "order_details"},
aviatorName: 'orderConfirmation',
aviatorArgs: {'orderId': order.id}
);
} catch (e) {
emitFailure(
newState: bloc.state.copyWith(error: e.toString()),
groupsToRebuild: {"checkout_status"}
);
}
}
}
Best Practices
- Single Responsibility
- Each use case should do one thing well
- Break complex operations into multiple use cases
- Use composition for complex flows
- Error Handling
- Always handle errors appropriately
- Use specific error types
- Provide meaningful error messages
- State Updates
- Be specific with groupsToRebuild
- Update only what’s necessary
- Consider derived state impacts
- Resource Management
- Clean up resources in close()
- Cancel subscriptions properly
- Handle timeouts appropriately
- Testing
- Make use cases easily testable
- Mock dependencies properly
- Test error cases
Advanced Patterns
Chaining Use Cases
class SignUpUseCase extends BlocUseCase<AuthBloc, SignUpEvent> {
@override
Future<void> execute(SignUpEvent event) async {
// Chain multiple operations
await validateEmail();
await createUser();
await sendVerification();
await syncPreferences();
}
Future<void> validateEmail() async {
emitWaiting(groupsToRebuild: {"signup_status"});
// Validation logic
}
Future<void> createUser() async {
emitWaiting(groupsToRebuild: {"signup_status"});
// User creation logic
}
// Additional methods...
}
Use Case Coordination
class OrderCoordinatorUseCase extends BlocUseCase<OrderBloc, CreateOrderEvent> {
@override
Future<void> execute(CreateOrderEvent event) async {
// Coordinate multiple blocs
final cartBloc = resolver.resolve<CartBloc>();
final paymentBloc = resolver.resolve<PaymentBloc>();
final inventoryBloc = resolver.resolve<InventoryBloc>();
// Execute coordinated operations
await validateInventory(inventoryBloc);
await processPayment(paymentBloc);
await createOrder();
await clearCart(cartBloc);
}
}
Remember: Use cases are the heart of your application’s business logic. Take time to design them well and keep them focused and maintainable.