JuiceWidgetState
Overview
JuiceWidgetState
is Juice’s solution for widgets that need to maintain local UI state while still connecting to Juice’s reactive state management system. It combines Flutter’s StatefulWidget
capabilities with Juice’s bloc integration.
Comparison with StatelessJuiceWidget
Both widgets provide:
- Reactive updates to bloc state changes
- Smart rebuilding through groups
- Type-safe state access
- Built-in error handling
Key differences:
JuiceWidgetState
can maintain local widget state- Additional lifecycle methods (initState, dispose)
prepareForUpdate
method for state changes- More control over setState timing
When to Use JuiceWidgetState
Use JuiceWidgetState when your widget needs to:
- Maintain Local State ```dart class AnimatedCounter extends StatefulWidget { @override State
createState() => AnimatedCounterState(); }
class AnimatedCounterState extends JuiceWidgetState<CounterBloc, AnimatedCounter> { // Local animation controller late AnimationController _controller;
@override void initState() { super.initState(); _controller = AnimationController( vsync: this, // Can use vsync because we’re a State duration: Duration(milliseconds: 300), ); }
@override void prepareForUpdate(StreamStatus status) { // Animate on counter changes if (status is UpdatingStatus) { _controller.forward(from: 0); } }
@override Widget onBuild(BuildContext context, StreamStatus status) { return AnimatedBuilder( animation: _controller, builder: (context, child) { return Transform.scale( scale: 1.0 + (_controller.value * 0.2), child: Text(‘Count: ${bloc.state.count}’), ); }, ); }
@override void dispose() { _controller.dispose(); super.dispose(); } }
2. Handle Complex Input
```dart
class SearchField extends StatefulWidget {
@override
State<SearchField> createState() => SearchFieldState();
}
class SearchFieldState extends JuiceWidgetState<SearchBloc, SearchField> {
late TextEditingController _textController;
Timer? _debounceTimer;
@override
void initState() {
super.initState();
_textController = TextEditingController(text: bloc.state.query);
}
void _onSearchChanged(String query) {
_debounceTimer?.cancel();
_debounceTimer = Timer(Duration(milliseconds: 300), () {
bloc.send(SearchEvent(query: query));
});
}
@override
Widget onBuild(BuildContext context, StreamStatus status) {
return TextField(
controller: _textController,
onChanged: _onSearchChanged,
decoration: InputDecoration(
suffixIcon: status is WaitingStatus
? CircularProgressIndicator()
: Icon(Icons.search),
),
);
}
@override
void dispose() {
_textController.dispose();
_debounceTimer?.cancel();
super.dispose();
}
}
- Coordinate Multiple Animations/Controllers ```dart class ComplexAnimation extends StatefulWidget { @override State
createState() => ComplexAnimationState(); }
class ComplexAnimationState extends JuiceWidgetState<AnimationBloc, ComplexAnimation> with TickerProviderStateMixin { late AnimationController _slideController; late AnimationController _fadeController; late Animation
@override void initState() { super.initState(); _initializeAnimations(); }
void _initializeAnimations() { _slideController = AnimationController( vsync: this, duration: Duration(milliseconds: 500), );
_fadeController = AnimationController(
vsync: this,
duration: Duration(milliseconds: 300),
);
_slideAnimation = Tween<Offset>(
begin: Offset(-1.0, 0.0),
end: Offset.zero,
).animate(CurvedAnimation(
parent: _slideController,
curve: Curves.easeOut,
));
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(_fadeController); }
@override void prepareForUpdate(StreamStatus status) { if (status is UpdatingStatus) { // Coordinate animations based on state changes _slideController.forward(from: 0); Future.delayed( Duration(milliseconds: 200), () => _fadeController.forward(from: 0), ); } }
@override Widget onBuild(BuildContext context, StreamStatus status) { return SlideTransition( position: _slideAnimation, child: FadeTransition( opacity: _fadeAnimation, child: ContentView(data: bloc.state.data), ), ); }
@override void dispose() { _slideController.dispose(); _fadeController.dispose(); super.dispose(); } }
## Key Methods
### initState()
Called when the widget is first created. Use for initialization.
```dart
@override
void initState() {
super.initState();
// Initialize controllers, subscriptions, etc.
_scrollController = ScrollController();
_scrollController.addListener(_onScroll);
}
prepareForUpdate(StreamStatus status)
Called before setState when a state change is accepted. Perfect for preparing animations or updating local state.
@override
void prepareForUpdate(StreamStatus status) {
if (status is UpdatingStatus &&
status.state.selectedIndex != status.oldState.selectedIndex) {
// Prepare scroll position for new selection
_scrollController.animateTo(
status.state.selectedIndex * itemHeight,
duration: Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
}
onStateChange(StreamStatus status)
First level of control for rebuilds. Return false to prevent rebuild.
@override
bool onStateChange(StreamStatus status) {
// Only rebuild if visible items changed
if (status is UpdatingStatus) {
final visibleRange = _getVisibleRange();
return status.state.hasChangesInRange(visibleRange);
}
return true;
}
onBuild(BuildContext context, StreamStatus status)
Main build method, called when widget needs to rebuild.
@override
Widget onBuild(BuildContext context, StreamStatus status) {
return ListView.builder(
controller: _scrollController,
itemCount: bloc.state.items.length,
itemBuilder: (context, index) {
final item = bloc.state.items[index];
return ItemTile(
item: item,
isSelected: index == _selectedIndex,
onTap: () => setState(() => _selectedIndex = index),
);
},
);
}
dispose()
Clean up resources when widget is removed.
@override
void dispose() {
_scrollController.dispose();
_subscription?.cancel();
super.dispose();
}
Best Practices
- Proper Resource Management ```dart class ResourcefulWidget extends StatefulWidget { @override State
createState() => ResourcefulWidgetState(); }
class ResourcefulWidgetState extends JuiceWidgetState<DataBloc, ResourcefulWidget> { StreamSubscription? _subscription; final _controllers =
@override void dispose() { // Clean up all resources _subscription?.cancel(); for (final controller in _controllers) { controller.dispose(); } super.dispose(); } }
2. **Smart State Updates**
```dart
class OptimizedWidget extends StatefulWidget {
@override
State<OptimizedWidget> createState() => OptimizedWidgetState();
}
class OptimizedWidgetState extends JuiceWidgetState<DataBloc, OptimizedWidget> {
int _localValue = 0;
@override
bool onStateChange(StreamStatus status) {
// Skip rebuilds if local state is more important
if (_localValue > 0 && status is UpdatingStatus) {
return false;
}
return true;
}
@override
void prepareForUpdate(StreamStatus status) {
// Reset local state when global state changes
if (status is UpdatingStatus) {
setState(() => _localValue = 0);
}
}
}
- Separation of Concerns ```dart class WellOrganizedWidget extends StatefulWidget { @override State
createState() => WellOrganizedWidgetState(); }
class WellOrganizedWidgetState extends JuiceWidgetState<DataBloc, WellOrganizedWidget> { // Group related state final _animations = _AnimationGroup(); final _input = _InputHandlers();
@override void initState() { super.initState(); _animations.initialize(this); _input.initialize(); }
@override Widget onBuild(BuildContext context, StreamStatus status) { return Column( children: [ _buildAnimatedContent(), _buildInputSection(), ], ); }
Widget _buildAnimatedContent() { return _animations.build(bloc.state.data); }
Widget _buildInputSection() { return _input.build( onChanged: (value) => bloc.send(UpdateEvent(value)) ); }
@override void dispose() { _animations.dispose(); _input.dispose(); super.dispose(); } }
## Common Patterns
### Form Handling
```dart
class FormWidget extends StatefulWidget {
@override
State<FormWidget> createState() => FormWidgetState();
}
class FormWidgetState extends JuiceWidgetState<FormBloc, FormWidget> {
final _formKey = GlobalKey<FormState>();
late TextEditingController _nameController;
late TextEditingController _emailController;
@override
void initState() {
super.initState();
_nameController = TextEditingController(text: bloc.state.name);
_emailController = TextEditingController(text: bloc.state.email);
}
@override
void prepareForUpdate(StreamStatus status) {
if (status is UpdatingStatus) {
// Update controllers if server data changes
_nameController.text = status.state.name;
_emailController.text = status.state.email;
}
}
void _onSubmit() {
if (_formKey.currentState!.validate()) {
bloc.send(SubmitFormEvent(
name: _nameController.text,
email: _emailController.text,
));
}
}
@override
Widget onBuild(BuildContext context, StreamStatus status) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _nameController,
decoration: InputDecoration(labelText: 'Name'),
validator: (value) =>
value?.isEmpty ?? true ? 'Required' : null,
),
TextFormField(
controller: _emailController,
decoration: InputDecoration(labelText: 'Email'),
validator: _validateEmail,
),
ElevatedButton(
onPressed: status is! WaitingStatus ? _onSubmit : null,
child: status is WaitingStatus
? CircularProgressIndicator()
: Text('Submit'),
),
],
),
);
}
@override
void dispose() {
_nameController.dispose();
_emailController.dispose();
super.dispose();
}
}
Complex Animations
class AnimatedList extends StatefulWidget {
@override
State<AnimatedList> createState() => AnimatedListState();
}
class AnimatedListState extends JuiceWidgetState<ListBloc, AnimatedList>
with TickerProviderStateMixin {
final _listKey = GlobalKey<AnimatedListState>();
late List<AnimationController> _itemControllers;
@override
void initState() {
super.initState();
_initializeAnimations();
}
void _initializeAnimations() {
_itemControllers = List.generate(
bloc.state.items.length,
(index) => AnimationController(
vsync: this,
duration: Duration(milliseconds: 300),
)..forward(),
);
}
@override
void prepareForUpdate(StreamStatus status) {
if (status is UpdatingStatus) {
final oldItems = status.oldState.items;
final newItems = status.state.items;
// Handle item additions/removals with animations
_updateAnimations(oldItems, newItems);
}
}
void _updateAnimations(List<Item> oldItems, List<Item> newItems) {
// Add animations for new items
final newCount = newItems.length - oldItems.length;
if (newCount > 0) {
for (var i = 0; i < newCount; i++) {
final controller = AnimationController(
vsync: this,
duration: Duration(milliseconds: 300),
);
_itemControllers.add(controller);
controller.forward();
}
}
// Remove animations for deleted items
if (newCount < 0) {
for (var i = 0; i < -newCount; i++) {
final controller = _itemControllers.removeLast();
controller.dispose();
}
}
}
@override
Widget onBuild(BuildContext context, StreamStatus status) {
return AnimatedList(
key: _listKey,
itemBuilder: (context, index, animation) {
return FadeTransition(
opacity: _itemControllers[index],
child: SlideTransition(
position: animation.drive(Tween(
begin: Offset(1, 0),
end: Offset.zero,
)),
child: ItemTile(item: bloc.state.items[index]),
),
);
},
);
}
@override
void dispose() {
for (final controller in _itemControllers) {
controller.dispose();
}
super.dispose();
}
}
Summary
JuiceWidgetState provides:
- All the reactive benefits of StatelessJuiceWidget
- Full stateful widget capabilities
- Fine-grained control over state updates
- Rich lifecycle management
- Advanced animation control
When to Choose JuiceWidgetState
Use JuiceWidgetState when you need:
- Local UI state management (form inputs, scroll positions, etc.)
- Animation controllers or other resources requiring disposal
- Complex state update coordination
- Access to StatefulWidget lifecycle methods
Use StatelessJuiceWidget when:
- Widget only displays bloc state
- No local state is needed
- Simple UI updates suffice
- No resource management is required
Key Takeaways
- State Management
- Use local state (_variables) for UI-specific data
- Access bloc state through bloc.state
- Coordinate local and bloc state in prepareForUpdate
- Lifecycle Control
- Initialize resources in initState
- Clean up in dispose
- Use prepareForUpdate for state transition logic
- Handle rebuilds efficiently with onStateChange
- Resource Management
- Always dispose controllers and subscriptions
- Keep track of resources in instance variables
- Clean up resources before creating new ones
- Use proper mixin support (e.g., TickerProviderStateMixin)
- Performance Optimization
- Use rebuild groups effectively
- Implement onStateChange for rebuild control
- Batch setState calls when possible
- Clean up unused resources promptly
Example Decision Flow
// Decision flow for choosing between StatelessJuiceWidget and JuiceWidgetState
if (needsLocalState() ||
needsResourceManagement() ||
needsAnimationControllers() ||
needsLifecycleMethods()) {
// Use JuiceWidgetState
class MyWidget extends StatefulWidget {
@override
State<MyWidget> createState() => MyWidgetState();
}
class MyWidgetState extends JuiceWidgetState<MyBloc, MyWidget> {
// Local state, controllers, etc.
}
} else {
// Use StatelessJuiceWidget
class MyWidget extends StatelessJuiceWidget<MyBloc> {
// Just the build method
}
}
A Note About setState
Unlike regular StatefulWidget development where you frequently call setState(), JuiceWidgetState handles most state updates automatically. The framework:
- Automatically calls setState when bloc state changes (filtered through your groups and onStateChange)
- Manages the rebuild cycle through StreamStatus
- Coordinates updates through prepareForUpdate
You rarely need to call setState manually. Only use setState when:
- Updating purely local UI state that isn’t connected to bloc state
- Managing temporary visual states (like hover effects)
// ❌ Don't do this - Juice handles bloc state updates
@override
void prepareForUpdate(StreamStatus status) {
setState(() { // Unnecessary - Juice will rebuild automatically
// Handle bloc state update
});
}
// ✅ Do this - Let Juice handle the update
@override
void prepareForUpdate(StreamStatus status) {
// Prepare for update, no setState needed
_animationController.forward();
}
// ✅ Do this - setState for local UI state only
void _onHover(bool isHovered) {
setState(() => _isHovered = isHovered); // Local UI state
}
Remember: Juice’s state management system is designed to minimize manual setState calls and handle most rebuild scenarios automatically. Focus on using the provided lifecycle methods (onStateChange, prepareForUpdate) to coordinate updates rather than managing them manually.