In large Flutter applications, business logic can become complex and spread across multiple blocs. The use case layer (also called the interactor or domain layer) provides a dedicated place for business rules, orchestrating repositories and other dependencies. This guide explains how to implement use cases with Bloc to create scalable, testable applications.
What is a Use Case?
A use case represents a single, specific action that your application can perform – for example, "login with email and password" or "fetch user profile". It contains the business logic for that action, orchestrating one or more repositories. Use cases are typically stateless and receive input parameters, then return a result (or throw an error).
Why Use Cases with Bloc?
- Separation of concerns – Blocs focus on state management and event handling; use cases contain domain logic.
- Reusability – The same use case can be called from different blocs (e.g.,
FetchUserfrom bothProfileBlocandSettingsBloc). - Testability – Use cases are pure Dart classes that can be unit‑tested without any Flutter dependencies.
- Single responsibility – Each use case does one thing and does it well.
- Clarity – Business logic is explicit and easy to understand, even for non‑technical stakeholders.
Core Components
| Component | Responsibility |
|---|---|
| Use Case Interface | Defines the contract for a specific action (optional) |
| Use Case Implementation | Contains the business logic, uses repositories |
| Bloc | Calls use cases, handles results, emits states |
| Repository | Provides data, used by use cases |
Step-by-Step Implementation
Create a class that represents the use case. It should depend on repositories and expose an execute (or call) method. Use Either or a custom result type for error handling, or throw exceptions.
Inject the use case into the bloc and call it in the event handler. The bloc then emits states based on the result.
To have consistent error handling, define a hierarchy of failures (or exceptions).
Use Cases with Parameters
For use cases that require multiple parameters, define a simple parameter object to keep the API clean and allow easier extension.
Use Cases that Combine Multiple Repositories
One of the main benefits of use cases is orchestrating multiple repositories. For example, a RegisterUserUseCase might call both AuthRepository and AnalyticsRepository.
Dependency Injection for Use Cases
Use a service locator like get_it to register use cases and inject repositories. This makes them available to your blocs.
Testing Use Cases
Use cases are easy to test because they have no Flutter dependencies. You can mock repositories and verify the business logic.
Use Case Variations
Some use cases return a stream of data (e.g., real-time updates). The use case can expose a stream, and the bloc can subscribe to it.
You can add validation inside the use case before calling repositories, returning a ValidationFailure if input is invalid.
When to Use Use Cases
- Complex business logic – When a bloc would become large and contain many conditional branches.
- Shared logic – When the same business rule is needed in multiple blocs.
- Testing – When you want to test business logic without UI dependencies.
- Clean Architecture – When you want to strictly separate domain, data, and presentation layers.
When to Skip Use Cases
- Simple CRUD apps – If a bloc only calls a single repository method, the use case adds boilerplate.
- Prototypes – For speed, you can put logic directly in the bloc and refactor later.
- Very small teams/projects – Sometimes simplicity is better than perfect architecture.
Best Practices
- Keep use cases stateless – No mutable state inside a use case (it can be reused across calls).
- Use meaningful names –
LoginUseCase,FetchUserProfileUseCase– the name should describe the action. - Return a result type – Use
Eitheror a customResultto make error handling explicit. - Inject dependencies – Never instantiate repositories inside a use case.
- Test use cases thoroughly – Write unit tests for both success and error paths.
- Avoid UI-specific code – Use cases should never know about
BuildContext,Navigator, etc.
Common Mistakes
- ❌ Putting too much logic in a single use case – Violates single responsibility; split into smaller use cases.
- ❌ Returning data source exceptions directly – Wrap them in meaningful failures.
- ❌ Using use cases for trivial operations – Adds unnecessary indirection for simple calls.
- ❌ Storing state in use cases – Use cases should be stateless; keep state in blocs or repositories.
- ❌ Calling use cases from the UI directly – Use cases should be called by blocs, not directly from widgets.
What's Next?
Now that you have a solid domain layer, explore how to integrate it with the repository pattern and dependency injection to build a complete Clean Architecture app.
Next, explore Repository pattern with Bloc and Clean Architecture with Bloc.