State Management in Juice
BlocState Fundamentals
BlocState is the foundation of state management in Juice. All bloc states must extend BlocState:
// Basic state implementation
class CounterState extends BlocState {
final int count;
CounterState({required this.count});
CounterState copyWith({int? count}) {
return CounterState(count: count ?? this.count);
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CounterState &&
runtimeType == other.runtimeType &&
count == other.count;
@override
int get hashCode => count.hashCode;
}
Best Practices for State Design
- Make States Immutable ```dart class UserState extends BlocState { // Use final for all fields final User user; final List
orders;
// Use const constructor when possible const UserState({ required this.user, required this.orders, });
// Create an unmodifiable view of collections List
2. **Implement CopyWith Methods**
```dart
class UserState extends BlocState {
final User user;
final List<Order> orders;
final bool isVerified;
const UserState({
required this.user,
required this.orders,
this.isVerified = false,
});
UserState copyWith({
User? user,
List<Order>? orders,
bool? isVerified,
}) {
return UserState(
user: user ?? this.user,
orders: orders ?? this.orders,
isVerified: isVerified ?? this.isVerified,
);
}
}
- Implement Equality ```dart class UserState extends BlocState { final User user; final List
orders;
@override bool operator ==(Object other) => identical(this, other) || other is UserState && runtimeType == other.runtimeType && user == other.user && listEquals(orders, other.orders);
@override int get hashCode => Object.hash(user, Object.hashAll(orders)); }
## State Organization
### Nested States
When dealing with complex state, organize it into logical sub-states:
```dart
// Base state for auth feature
class AuthState extends BlocState {
final User? user;
final AuthStatus status;
final String? error;
const AuthState({
this.user,
required this.status,
this.error,
});
}
// Specific states for different auth scenarios
class SignInState extends AuthState {
final String email;
final bool isEmailValid;
const SignInState({
required this.email,
required this.isEmailValid,
required super.status,
super.error,
});
}
class SignUpState extends AuthState {
final String email;
final String? verificationCode;
final bool isCodeSent;
const SignUpState({
required this.email,
this.verificationCode,
required this.isCodeSent,
required super.status,
super.error,
});
}
State Composition
Break large states into manageable pieces:
// Main state composed of smaller states
class AppState extends BlocState {
final UserState userState;
final PreferencesState prefsState;
final NavigationState navState;
const AppState({
required this.userState,
required this.prefsState,
required this.navState,
});
static AppState initial() => AppState(
userState: UserState.initial(),
prefsState: PreferencesState.initial(),
navState: NavigationState.initial(),
);
}
// Helper methods for state updates
class AppState extends BlocState {
AppState updateUser(User newUser) {
return copyWith(
userState: userState.copyWith(user: newUser),
);
}
AppState updatePreference(String key, dynamic value) {
return copyWith(
prefsState: prefsState.updatePreference(key, value),
);
}
}
State Updates
Through Use Cases
The primary way to update state is through use cases:
class UpdateUserUseCase extends BlocUseCase<ProfileBloc, UpdateUserEvent> {
@override
Future<void> execute(UpdateUserEvent event) async {
// Show loading state
emitWaiting(groupsToRebuild: {"profile"});
try {
final updatedUser = await userService.updateUser(event.userUpdate);
// Update state with new user data
emitUpdate(
newState: bloc.state.copyWith(user: updatedUser),
groupsToRebuild: {"profile", "header"},
);
} catch (e) {
emitFailure(groupsToRebuild: {"profile"});
}
}
}
Coordinating Multiple States
When updates affect multiple parts of the app:
class CompleteOrderUseCase extends BlocUseCase<OrderBloc, CompleteOrderEvent> {
@override
Future<void> execute(CompleteOrderEvent event) async {
emitWaiting();
try {
// Update order state
final updatedOrder = await orderService.complete(event.orderId);
// Notify interested blocs through relay
bloc.send(OrderCompletedEvent(order: updatedOrder));
// Update local state
emitUpdate(
newState: bloc.state.copyWith(
currentOrder: updatedOrder,
completedOrders: [...bloc.state.completedOrders, updatedOrder],
),
groupsToRebuild: {"orders", "status"},
);
} catch (e) {
emitFailure();
}
}
}
Advanced State Patterns
Derived State
Compute derived state through getters:
class OrderState extends BlocState {
final List<Order> orders;
final Map<String, OrderStatus> statusMap;
// Derived states
List<Order> get pendingOrders =>
orders.where((o) => o.status == OrderStatus.pending).toList();
double get totalRevenue =>
orders.fold(0, (sum, order) => sum + order.total);
Map<OrderStatus, int> get ordersByStatus =>
orders.fold({}, (map, order) {
map[order.status] = (map[order.status] ?? 0) + 1;
return map;
});
}
State History
Track state changes for undo/redo:
class EditorState extends BlocState {
final Document currentDocument;
final List<Document> history;
final int historyIndex;
bool get canUndo => historyIndex > 0;
bool get canRedo => historyIndex < history.length - 1;
EditorState undo() {
if (!canUndo) return this;
return copyWith(
currentDocument: history[historyIndex - 1],
historyIndex: historyIndex - 1,
);
}
EditorState redo() {
if (!canRedo) return this;
return copyWith(
currentDocument: history[historyIndex + 1],
historyIndex: historyIndex + 1,
);
}
EditorState addChange(Document newDocument) {
// Remove any redo history
final newHistory = history.sublist(0, historyIndex + 1);
return copyWith(
currentDocument: newDocument,
history: [...newHistory, newDocument],
historyIndex: historyIndex + 1,
);
}
}
State Validation
Add validation logic to states:
class FormState extends BlocState {
final String email;
final String password;
final String? confirmPassword;
bool get isEmailValid =>
email.isNotEmpty && email.contains('@');
bool get isPasswordValid =>
password.length >= 8;
bool get doPasswordsMatch =>
confirmPassword != null && password == confirmPassword;
bool get isFormValid =>
isEmailValid && isPasswordValid && doPasswordsMatch;
List<String> get validationErrors {
final errors = <String>[];
if (!isEmailValid) errors.add('Invalid email');
if (!isPasswordValid) errors.add('Password too short');
if (!doPasswordsMatch) errors.add('Passwords do not match');
return errors;
}
}
Best Practices Summary
- Keep States Immutable
- Use final fields
- Return new instances on updates
- Use const constructors when possible
- Design for Change
- Implement thorough copyWith methods
- Consider future state needs
- Break complex states into sub-states
- Optimize for Performance
- Implement proper equality
- Use derived state for computations
- Consider memoization for expensive calculations
- Think About Maintenance
- Document state structure
- Keep states focused
- Use clear naming conventions
- Handle Edge Cases
- Consider null states
- Add validation
- Track state transitions
Remember: Good state design is crucial for maintainable applications. Take time to plan your state structure and update patterns.