Getting Started with Juice
This guide will walk you through creating your first Juice application. By the end, you’ll understand the core concepts and be ready to build more complex applications.
Installation
- Add Juice to your
pubspec.yaml
:
dependencies:
juice: ^1.0.4
- Or run:
flutter pub add juice
- Import Juice in your code:
import 'package:juice/juice.dart';
Creating Your First Juice App
Let’s create a counter app that demonstrates Juice’s key features. We’ll build it step by step.
1. Define Your State
First, create a state class that holds your application’s data:
class CounterState extends BlocState {
final int count;
CounterState({required this.count});
CounterState copyWith({int? count}) {
return CounterState(count: count ?? this.count);
}
}
2. Create Events
Define events that represent user actions:
class IncrementEvent extends EventBase {}
class DecrementEvent extends EventBase {}
3. Write Use Cases
Create use cases that handle your business logic:
class IncrementUseCase extends BlocUseCase<CounterBloc, IncrementEvent> {
@override
Future<void> execute(IncrementEvent event) async {
// Show loading state while we "process"
emitWaiting(groupsToRebuild: {"counter"});
// Simulate some work
await Future.delayed(Duration(milliseconds: 2000));
// Update the state
final newState = bloc.state.copyWith(count: bloc.state.count + 1);
emitUpdate(
newState: newState,
groupsToRebuild: {"counter"}
);
}
}
class DecrementUseCase extends BlocUseCase<CounterBloc, DecrementEvent> {
@override
Future<void> execute(DecrementEvent event) async {
emitWaiting(groupsToRebuild: {"counter"});
await Future.delayed(Duration(milliseconds: 2000));
final newState = bloc.state.copyWith(count: bloc.state.count - 1);
emitUpdate(
newState: newState,
groupsToRebuild: {"counter"}
);
}
}
4. Create Your Bloc
Set up your bloc to coordinate state and use cases:
class CounterBloc extends JuiceBloc<CounterState> {
CounterBloc()
: super(
CounterState(count: 0), // Initial state
[
// Register use cases
() => UseCaseBuilder(
typeOfEvent: IncrementEvent,
useCaseGenerator: () => IncrementUseCase()),
() => UseCaseBuilder(
typeOfEvent: DecrementEvent,
useCaseGenerator: () => DecrementUseCase()),
],
[], // No navigation for this simple example
);
}
5. Create Your Widgets
Create widgets that display your UI and respond to state changes:
// Display widget that shows the counter
class CounterDisplay extends StatelessJuiceWidget<CounterBloc> {
CounterDisplay({super.key, super.groups = const {"counter"}});
@override
Widget onBuild(BuildContext context, StreamStatus status) {
return status.when(
updating: (state, _, __) => Text(
'Count: ${state.count}',
style: Theme.of(context).textTheme.headlineMedium,
),
waiting: (_, __, ___) => CircularProgressIndicator(),
error: (_, __, ___) => Text('Error occurred'),
canceling: (_, __, ___) => Text('Operation cancelled'),
);
}
}
// Button widget that triggers state changes
class CounterButtons extends StatelessJuiceWidget<CounterBloc> {
CounterButtons({super.key, super.groups = const {}}); // Don't rebuild on state changes
@override
Widget onBuild(BuildContext context, StreamStatus status) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => bloc.send(IncrementEvent()),
child: Icon(Icons.add),
),
SizedBox(width: 16),
ElevatedButton(
onPressed: () => bloc.send(DecrementEvent()),
child: Icon(Icons.remove),
),
],
);
}
}
6. Put It All Together
Create your main app:
void main() {
// Initialize Juice
GlobalBlocResolver().resolver = BlocResolver();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Juice Counter')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CounterDisplay(),
SizedBox(height: 16),
CounterButtons(),
],
),
),
),
);
}
}
Key Concepts Demonstrated
This simple example shows several key Juice features:
- State Management
- Clean state class with immutable updates
- State changes through events and use cases
- Reactive UI updates
- Use Cases
- Isolated business logic
- Handling async operations
- State update control
- Smart Rebuilds
- Group-based widget updates
- Loading state handling
- Error state management
- Clean Architecture
- Clear separation of concerns
- Testable components
- Maintainable structure
Next Steps
Now that you’ve built your first Juice app, you can:
- Learn about Use Cases in Depth
- Explore State Management
- See more Examples
- Read about Navigation
Common Questions
How do widgets access state in Juice?
Juice widgets have direct, type-safe access to their bloc’s state through the bloc.state property (or bloc1.state, bloc2.state, etc. for multi-bloc widgets). see Accessing State in Juice-aware Widgets
When should widgets rebuild?
Use the groups
parameter to control which state changes trigger rebuilds. Use empty groups (const {}
) for widgets that don’t need to rebuild on state changes.
Why use use cases?
Use cases isolate business logic, making your code easier to test, maintain, and modify. They also provide a clear place for handling loading states, errors, and cancellation.
What’s StreamStatus?
StreamStatus helps manage different UI states (updating, waiting, error, canceling) in a type-safe way. Use the when
method to handle each state appropriately. see StreamStatus Patterns
How do I handle errors?
Use cases can emit failure states using emitFailure()
. Handle these in your widgets using the error
case in status.when()
.