Working with StreamStatus
StreamStatus helps manage different UI states (updating, waiting, error, canceling). For simple widgets, the when
method works well:
// Simple widget using when pattern
@override
Widget onBuild(BuildContext context, StreamStatus status) {
return status.when(
updating: (_) => Text('Count: ${bloc.state.count}'),
waiting: (_) => CircularProgressIndicator(),
error: (_) => Text('Error occurred'),
canceling: (_) => Text('Operation cancelled'),
);
}
However, for complex UIs, there are better patterns:
Pattern 1: Status-Aware Component Methods
Break your UI into logical pieces and handle status per component:
class ComplexProfileWidget extends StatelessJuiceWidget<ProfileBloc> {
@override
Widget onBuild(BuildContext context, StreamStatus status) {
return Column(
children: [
_buildHeader(context, status),
_buildBody(context, status),
_buildActions(context, status),
],
);
}
Widget _buildHeader(BuildContext context, StreamStatus status) {
final profile = bloc.state.profile;
// Handle loading state inline where needed
if (status is WaitingStatus) {
return ShimmerLoading(child: ProfileHeaderPlaceholder());
}
return ProfileHeader(
title: profile.name,
subtitle: profile.email,
// Disable actions during certain states
enabled: status is! WaitingStatus && status is! CancelingStatus,
);
}
Widget _buildBody(BuildContext context, StreamStatus status) {
// Access state directly when loading state doesn't matter
return ProfileDetails(
data: bloc.state.details,
// Pass status-dependent properties
isEditable: status is! WaitingStatus,
);
}
Widget _buildActions(BuildContext context, StreamStatus status) {
// Use status type checks for enabling/disabling
return Row(
children: [
ElevatedButton(
onPressed: status is! WaitingStatus
? () => bloc.send(SaveProfileEvent())
: null,
child: Text('Save'),
),
if (status is WaitingStatus)
LoadingSpinner(),
],
);
}
}
Pattern 2: Status-Specific Overlays
Keep your main UI stable and overlay status indicators:
class DataGridWidget extends StatelessJuiceWidget<GridBloc> {
@override
Widget onBuild(BuildContext context, StreamStatus status) {
return Stack(
children: [
// Main content always visible
_buildMainContent(bloc.state),
// Status overlays
if (status is WaitingStatus)
LoadingOverlay(message: 'Loading data...'),
if (status is FailureStatus)
ErrorOverlay(
message: 'Failed to load data',
onRetry: () => bloc.send(RetryLoadEvent()),
),
if (status is CancelingStatus)
CancelOverlay(message: 'Operation cancelled'),
],
);
}
Widget _buildMainContent(GridState state) {
return GridView.builder(
itemCount: state.items.length,
itemBuilder: (context, index) => ItemCard(item: state.items[index]),
);
}
}
Pattern 3: Status-Dependent Behavior
Encapsulate status-dependent logic in helper methods:
class OrderFormWidget extends StatelessJuiceWidget<OrderBloc> {
@override
Widget onBuild(BuildContext context, StreamStatus status) {
return Form(
child: Column(
children: [
OrderDetailsForm(
initialData: bloc.state.details,
enabled: _isFormEditable(status),
),
AddressSection(
address: bloc.state.address,
onEdit: _canEditAddress(status)
? () => bloc.send(EditAddressEvent())
: null,
),
PaymentSection(
paymentMethods: bloc.state.paymentMethods,
selectedMethod: bloc.state.selectedMethod,
onSelect: _canChangePayment(status)
? (method) => bloc.send(SelectPaymentEvent(method))
: null,
),
SubmitButton(
onPressed: _canSubmit(status)
? () => bloc.send(SubmitOrderEvent())
: null,
child: _getSubmitButtonChild(status),
),
],
),
);
}
bool _isFormEditable(StreamStatus status) {
return status is! WaitingStatus &&
status is! CancelingStatus &&
bloc.state.orderStatus != OrderStatus.submitted;
}
bool _canEditAddress(StreamStatus status) {
return _isFormEditable(status) &&
!bloc.state.isExpressCheckout;
}
bool _canChangePayment(StreamStatus status) {
return _isFormEditable(status) &&
bloc.state.paymentMethods.isNotEmpty;
}
bool _canSubmit(StreamStatus status) {
return status is! WaitingStatus &&
status is! CancelingStatus &&
bloc.state.isValid &&
bloc.state.orderStatus == OrderStatus.draft;
}
Widget _getSubmitButtonChild(StreamStatus status) {
if (status is WaitingStatus) {
return LoadingSpinner();
}
return Text('Submit Order');
}
}
Pattern 4: Composite Status Handling
For widgets that need to coordinate multiple blocs:
class CheckoutWidget extends StatelessJuiceWidget2<OrderBloc, PaymentBloc> {
@override
Widget onBuild(BuildContext context, StreamStatus status) {
// Handle complex loading states
final isOrderProcessing = bloc1.currentStatus is WaitingStatus;
final isPaymentProcessing = bloc2.currentStatus is WaitingStatus;
if (isOrderProcessing && isPaymentProcessing) {
return FullPageLoader(message: 'Processing your order...');
}
return Column(
children: [
// Order summary always visible
OrderSummary(order: bloc1.state.order),
// Payment section with its own loading state
PaymentSection(
methods: bloc2.state.methods,
isLoading: isPaymentProcessing,
),
// Status-aware actions
CheckoutActions(
onSubmit: _canSubmit ? _handleSubmit : null,
isProcessing: isOrderProcessing,
),
],
);
}
bool get _canSubmit {
final orderStatus = bloc1.currentStatus;
final paymentStatus = bloc2.currentStatus;
return orderStatus is! WaitingStatus &&
paymentStatus is! WaitingStatus &&
bloc1.state.isValid &&
bloc2.state.selectedMethod != null;
}
}
These patterns help manage complex UIs while keeping your code organized and maintainable. Choose the pattern that best fits your widget’s complexity and requirements.