Smart Widget Rebuilding in Juice
Juice provides a powerful group-based system for controlling exactly which widgets rebuild in response to state changes. This system helps optimize performance by preventing unnecessary rebuilds while keeping code clean and maintainable.
Core Concepts
Rebuild Groups
Every Juice widget can specify which groups it belongs to:
class ProfileHeader extends StatelessJuiceWidget<ProfileBloc> {
// Widget rebuilds when "profile_header" group is triggered
ProfileHeader({super.key, super.groups = const {"profile_header"}});
@override
Widget onBuild(BuildContext context, StreamStatus status) {
return Text(bloc.state.userName);
}
}
Special Group Values
// Always rebuild (default)
const Set<String> rebuildAlways = {"*"};
// Never rebuild
const Set<String> optOutOfRebuilds = {"-"};
Configuring Widgets
Single Group
// Basic group membership
class UserAvatar extends StatelessJuiceWidget<UserBloc> {
UserAvatar({super.key, super.groups = const {"avatar"}});
}
Multiple Groups
// Widget participates in multiple groups
class UserStats extends StatelessJuiceWidget<UserBloc> {
UserStats({
super.key,
super.groups = const {"stats", "achievements"}
});
}
Opting Out
// Widget never rebuilds from state changes
class StaticHeader extends StatelessJuiceWidget<AppBloc> {
StaticHeader({super.key, super.groups = optOutOfRebuilds});
}
Always Rebuild
// Widget rebuilds on all state changes
class DebugPanel extends StatelessJuiceWidget<AppBloc> {
DebugPanel({super.key, super.groups = rebuildAlways});
}
Emitting Updates
Basic Emit
class UpdateProfileUseCase extends BlocUseCase<ProfileBloc, UpdateProfileEvent> {
@override
Future<void> execute(UpdateProfileEvent event) async {
// Only profile-related widgets rebuild
emitUpdate(
newState: bloc.state.copyWith(name: event.name),
groupsToRebuild: {"profile_header", "profile_details"}
);
}
}
Multiple Groups
class CompleteAchievementUseCase extends BlocUseCase<UserBloc, AchievementEvent> {
@override
Future<void> execute(AchievementEvent event) async {
// Update both achievement and stats widgets
emitUpdate(
newState: bloc.state.withNewAchievement(event.achievement),
groupsToRebuild: {"achievements", "stats", "profile_header"}
);
}
}
Status-Specific Rebuilds
class LoadProfileUseCase extends BlocUseCase<ProfileBloc, LoadProfileEvent> {
@override
Future<void> execute(LoadProfileEvent event) async {
// Show loading in header only
emitWaiting(groupsToRebuild: {"profile_header"});
try {
final profile = await profileService.load();
// Update all profile widgets
emitUpdate(
newState: bloc.state.copyWith(profile: profile),
groupsToRebuild: {
"profile_header",
"profile_details",
"profile_stats"
}
);
} catch (e) {
// Show error in header only
emitFailure(groupsToRebuild: {"profile_header"});
}
}
}
Real-World Examples
Chat Interface
// Message list only updates for new messages
class MessageList extends StatelessJuiceWidget<ChatBloc> {
MessageList({super.key, super.groups = const {"messages"}});
@override
Widget onBuild(BuildContext context, StreamStatus status) {
return ListView(
children: bloc.state.messages.map(buildMessage).toList(),
);
}
}
// Status updates independently
class ChatStatus extends StatelessJuiceWidget<ChatBloc> {
ChatStatus({super.key, super.groups = const {"chat_status"}});
@override
Widget onBuild(BuildContext context, StreamStatus status) {
return Text(bloc.state.statusText);
}
}
// Send message use case coordinates updates
class SendMessageUseCase extends BlocUseCase<ChatBloc, SendMessageEvent> {
@override
Future<void> execute(SendMessageEvent event) async {
// Update just the status
emitWaiting(groupsToRebuild: {"chat_status"});
try {
await chatService.send(event.message);
// Update both messages and status
emitUpdate(
newState: bloc.state.withNewMessage(event.message),
groupsToRebuild: {"messages", "chat_status"}
);
} catch (e) {
// Show error only in status
emitFailure(groupsToRebuild: {"chat_status"});
}
}
}
Form With Validation
// Form fields update independently
class EmailField extends StatelessJuiceWidget<FormBloc> {
EmailField({super.key, super.groups = const {"email_field"}});
}
class PasswordField extends StatelessJuiceWidget<FormBloc> {
PasswordField({super.key, super.groups = const {"password_field"}});
}
// Submit button depends on all fields
class SubmitButton extends StatelessJuiceWidget<FormBloc> {
SubmitButton({
super.key,
super.groups = const {"email_field", "password_field"}
});
}
// Validation use case updates specific fields
class ValidateEmailUseCase extends BlocUseCase<FormBloc, ValidateEmailEvent> {
@override
Future<void> execute(ValidateEmailEvent event) async {
emitWaiting(groupsToRebuild: {"email_field"});
try {
final isValid = await validateEmail(event.email);
emitUpdate(
newState: bloc.state.copyWith(
email: event.email,
isEmailValid: isValid
),
groupsToRebuild: {"email_field"}
);
} catch (e) {
emitFailure(groupsToRebuild: {"email_field"});
}
}
}
Best Practices
Group Naming
// ✅ Good: Clear, specific group names
const groups = {
"profile_header",
"profile_details",
"achievement_list"
};
// ❌ Bad: Vague, ambiguous names
const groups = {
"header", // Too generic
"data", // Too vague
"list" // Not specific enough
};
Group Organization
// Define groups as constants
abstract class ProfileGroups {
static const header = "profile_header";
static const details = "profile_details";
static const stats = "profile_stats";
// Related groups
static const all = {header, details, stats};
static const summary = {header, stats};
}
// Use in widgets
class ProfileHeader extends StatelessJuiceWidget<ProfileBloc> {
ProfileHeader({
super.key,
super.groups = const {ProfileGroups.header}
});
}
// Use in use cases
class UpdateProfileUseCase extends BlocUseCase<ProfileBloc, UpdateEvent> {
@override
Future<void> execute(UpdateEvent event) async {
emitUpdate(
newState: bloc.state.copyWith(profile: event.profile),
groupsToRebuild: ProfileGroups.all
);
}
}
Performance Optimization
// Only update what's necessary
class UpdateAvatarUseCase extends BlocUseCase<ProfileBloc, UpdateAvatarEvent> {
@override
Future<void> execute(UpdateAvatarEvent event) async {
// Don't rebuild entire profile, just avatar-related widgets
emitUpdate(
newState: bloc.state.copyWith(avatar: event.avatar),
groupsToRebuild: {"avatar", "header_avatar"}
);
}
}
// Use optOutOfRebuilds for static content
class ProfileLayout extends StatelessJuiceWidget<ProfileBloc> {
ProfileLayout({super.key, super.groups = optOutOfRebuilds});
@override
Widget onBuild(BuildContext context, StreamStatus status) {
return Column(
children: [
ProfileHeader(), // Rebuilds with "header" group
ProfileContent(), // Rebuilds with "content" group
const StaticFooter(), // Never rebuilds
],
);
}
}
Testing
void main() {
test('use case updates correct groups', () async {
final useCase = UpdateProfileUseCase();
final event = UpdateProfileEvent(name: 'Test');
// Mock emit methods to capture rebuilds
var capturedGroups = <String>{};
useCase.emitUpdate = ({
required Set<String> groupsToRebuild,
required BlocState newState
}) {
capturedGroups = groupsToRebuild;
};
await useCase.execute(event);
expect(
capturedGroups,
equals({"profile_header", "profile_details"})
);
});
}
Common Patterns
Loading States
class LoadDataUseCase extends BlocUseCase<DataBloc, LoadDataEvent> {
@override
Future<void> execute(LoadDataEvent event) async {
// Show loading spinner only in content area
emitWaiting(groupsToRebuild: {"content"});
try {
final data = await fetchData();
// Update content and summary
emitUpdate(
newState: bloc.state.copyWith(data: data),
groupsToRebuild: {"content", "summary"}
);
} catch (e) {
// Show error only in content area
emitFailure(groupsToRebuild: {"content"});
}
}
}
Progressive Updates
class ProcessOrderUseCase extends BlocUseCase<OrderBloc, ProcessOrderEvent> {
@override
Future<void> execute(ProcessOrderEvent event) async {
// Update status
emitUpdate(
newState: bloc.state.copyWith(status: 'Validating'),
groupsToRebuild: {"order_status"}
);
// Process steps
await validateOrder();
emitUpdate(
newState: bloc.state.copyWith(status: 'Processing Payment'),
groupsToRebuild: {"order_status"}
);
await processPayment();
emitUpdate(
newState: bloc.state.copyWith(
status: 'Complete',
isProcessed: true
),
groupsToRebuild: {"order_status", "order_summary"}
);
}
}
Remember: The group-based rebuild system is one of Juice’s most powerful features for performance optimization. Use it thoughtfully to create responsive, efficient UIs.