Bloc Basics in Juice
Blocs are the foundation of state management in Juice. They manage your application’s state and coordinate business logic through use cases.
Creating a Basic Bloc
A Juice bloc consists of three main components:
- The bloc class itself that extends
JuiceBloc
- A state class that extends
BlocState
- Events that extend
EventBase
Here’s a simple example:
// State class
class CounterState extends BlocState {
final int count;
CounterState({required this.count});
CounterState copyWith({int? count}) {
return CounterState(count: count ?? this.count);
}
}
// Event
class IncrementEvent extends EventBase {}
// Bloc
class CounterBloc extends JuiceBloc<CounterState> {
CounterBloc() : super(
CounterState(count: 0), // Initial state
[
() => UseCaseBuilder(
typeOfEvent: IncrementEvent,
useCaseGenerator: () => IncrementUseCase(),
),
],
[], // Aviators (navigation handlers)
);
}
State Design
States in Juice should be:
- Immutable: Once created, state objects should not change
- Simple: Only contain data needed by the feature the bloc is representing
- Copyable: Implement
copyWith
for easy state updates
class UserState extends BlocState {
final String name;
final int age;
final List<String> permissions;
// Immutable constructor
const UserState({
required this.name,
required this.age,
required this.permissions,
});
// Copyable
UserState copyWith({
String? name,
int? age,
List<String>? permissions,
}) {
return UserState(
name: name ?? this.name,
age: age ?? this.age,
permissions: permissions ?? this.permissions,
);
}
}
Events
Events trigger state changes and use case execution. They should:
- Be Clear: Name events based on intent (e.g.,
UpdateProfileEvent
) - Contain Data: Include any data needed by use cases
- Be Immutable: Once created, event data should not change
class UpdateProfileEvent extends EventBase {
final String name;
final int age;
const UpdateProfileEvent({
required this.name,
required this.age,
});
}
// For operations that can be cancelled
class UploadFileEvent extends CancellableEvent {
final File file;
final String destination;
const UploadFileEvent({
required this.file,
required this.destination,
});
}
Advanced Bloc Features
Group-Based Updates
Control which widgets rebuild when state changes:
class ProfileBloc extends JuiceBloc<ProfileState> {
void updateProfile() {
emitUpdate(
newState: newState,
groupsToRebuild: {'profile_details'}, // Only rebuild profile widgets
);
}
}
Cancellable Operations
Handle long-running operations with cancellation support:
class DownloadBloc extends JuiceBloc<DownloadState> {
Future<void> startDownload() async {
final operation = sendCancellable(DownloadEvent(...));
// Later...
operation.cancel(); // Cancel the operation
}
}
StreamStatus States
Juice blocs emit StreamStatus
objects that include state and status information:
class ProfileWidget extends StatelessJuiceWidget<ProfileBloc> {
@override
Widget onBuild(BuildContext context, StreamStatus status) {
// status.when is fine for single-bloc widgets
return status.when(
updating: (state, _, __) => ProfileView(data: bloc.state.profile),
waiting: (_, __, ___) => LoadingSpinner(),
error: (_, __, ___) => ErrorMessage(),
canceling: (_, __, ___) => CancellingMessage(),
);
}
}
Best Practices
- State Design
- Keep states focused on single features
- Use immutable objects (
final
fields) - Implement meaningful equality and toString
- Event Design
- One event per user action or system event
- Include all needed data in the event
- Use
CancellableEvent
for long operations
- Bloc Organization
- One bloc per feature or screen
- Keep blocs focused and single-purpose
- Use use cases for business logic
- Performance
- Use targeted group rebuilds
- Avoid large state objects
- Clean up resources in dispose
Common Pitfalls
- Mutable State ```dart // ❌ Bad: Mutable state class BadState extends BlocState { List
items = []; // Mutable list }
// ✅ Good: Immutable state class GoodState extends BlocState { final List
2. **Direct State Modification**
```dart
// ❌ Bad: Modifying state directly
bloc.state.items.add('new item');
// ✅ Good: Creating new state
emitUpdate(
newState: bloc.state.copyWith(
items: [...bloc.state.items, 'new item'],
),
);
- Mixing Business Logic ```dart // ❌ Bad: Logic in bloc class BadBloc extends JuiceBloc
{ void processData() { // Business logic here } }
// ✅ Good: Logic in use cases class ProcessDataUseCase extends BlocUseCase<Bloc, Event> { @override Future
Next Steps
- Learn about Use Cases for handling business logic
- Explore StreamStatus for managing UI states