Files
iiEsaywebUIapp/lib/shared/widgets/loading_states.dart
cogwheel0 1ea85d5ed1 refactor: enhance theme and error handling across the application
- Updated error handling in EnhancedErrorService to utilize context for color tokens, improving theme consistency.
- Refactored various components to use context-aware shadow and color properties, enhancing visual coherence.
- Replaced hardcoded color values with dynamic tokens in multiple widgets, ensuring better adaptability to theme changes.
- Improved overall code maintainability by centralizing theme-related logic and reducing direct dependencies on static theme values.
2025-10-03 00:12:25 +05:30

457 lines
12 KiB
Dart

import 'package:flutter/material.dart';
import '../theme/theme_extensions.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'dart:io' show Platform;
import '../services/brand_service.dart';
import '../theme/color_tokens.dart';
import 'package:conduit/l10n/app_localizations.dart';
/// Standard loading indicators following Conduit design patterns
class ConduitLoading {
// Private constructor to prevent instantiation
ConduitLoading._();
/// Primary loading indicator
static Widget primary({
double size = IconSize.lg,
Color? color,
String? message,
}) {
return _LoadingIndicator(
size: size,
color: color,
message: message,
type: _LoadingType.primary,
);
}
/// Inline loading for content areas
static Widget inline({
double size = IconSize.md,
Color? color,
String? message,
BuildContext? context,
}) {
return _LoadingIndicator(
size: size,
color:
color ??
(context?.conduitTheme.loadingIndicator ??
context?.conduitTheme.buttonPrimary ??
BrandService.primaryBrandColor(context: context)),
message: message,
type: _LoadingType.inline,
);
}
/// Button loading state
static Widget button({
double size = IconSize.sm,
Color? color,
BuildContext? context,
}) {
final tokens = context?.colorTokens ?? AppColorTokens.fallback();
return _LoadingIndicator(
size: size,
color:
color ??
(context?.conduitTheme.buttonPrimaryText ??
context?.conduitTheme.textPrimary ??
tokens.neutralTone00),
type: _LoadingType.button,
);
}
/// Overlay loading for full screen
static Widget overlay({String? message, bool darkBackground = true}) {
return _LoadingOverlay(message: message, darkBackground: darkBackground);
}
/// Skeleton loading for content placeholders
static Widget skeleton({
double width = double.infinity,
double height = 20,
BorderRadius? borderRadius,
}) {
return _SkeletonLoader(
width: width,
height: height,
borderRadius: borderRadius ?? BorderRadius.circular(AppBorderRadius.xs),
);
}
/// List item skeleton
static Widget listItemSkeleton({bool showAvatar = true, int lines = 2}) {
return _ListItemSkeleton(showAvatar: showAvatar, lines: lines);
}
}
enum _LoadingType { primary, inline, button }
class _LoadingIndicator extends StatelessWidget {
final double size;
final Color? color;
final String? message;
final _LoadingType type;
const _LoadingIndicator({
required this.size,
this.color,
this.message,
required this.type,
});
@override
Widget build(BuildContext context) {
final resolvedColor = color ?? context.conduitTheme.loadingIndicator;
Widget indicator;
if (Platform.isIOS) {
indicator = CupertinoActivityIndicator(
color: resolvedColor,
radius: size / 2,
);
} else {
indicator = SizedBox(
width: size,
height: size,
child: CircularProgressIndicator(
strokeWidth: size / 8,
valueColor: AlwaysStoppedAnimation<Color>(resolvedColor),
),
);
}
if (message == null) {
return indicator;
}
final spacing = type == _LoadingType.button ? Spacing.sm : Spacing.xs;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
indicator,
SizedBox(height: spacing),
Text(
message!,
style: TextStyle(
color: color,
fontSize: type == _LoadingType.button
? AppTypography.bodySmall
: AppTypography.bodyLarge,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
],
);
}
}
class _LoadingOverlay extends StatelessWidget {
final String? message;
final bool darkBackground;
const _LoadingOverlay({this.message, required this.darkBackground});
@override
Widget build(BuildContext context) {
return Container(
color: darkBackground
? context.conduitTheme.surfaceBackground.withValues(
alpha: Alpha.strong,
)
: context.conduitTheme.surfaceBackground.withValues(
alpha: Alpha.intense,
),
child: Center(
child: Container(
padding: const EdgeInsets.all(Spacing.lg),
decoration: BoxDecoration(
color: darkBackground
? context.conduitTheme.surfaceBackground
: context.conduitTheme.surfaceBackground,
borderRadius: BorderRadius.circular(AppBorderRadius.lg),
boxShadow: ConduitShadows.high(context),
),
child: ConduitLoading.primary(
size: IconSize.xl,
color: context.conduitTheme.buttonPrimary,
message: message,
),
),
),
).animate().fadeIn(duration: AnimationDuration.fast);
}
}
class _SkeletonLoader extends StatefulWidget {
final double width;
final double height;
final BorderRadius borderRadius;
const _SkeletonLoader({
required this.width,
required this.height,
required this.borderRadius,
});
@override
State<_SkeletonLoader> createState() => _SkeletonLoaderState();
}
class _SkeletonLoaderState extends State<_SkeletonLoader>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: AnimationDuration.ultra,
vsync: this,
);
_animation =
Tween<double>(
begin: AnimationValues.shimmerBegin,
end: AnimationValues.shimmerEnd,
).animate(
CurvedAnimation(
parent: _controller,
curve: AnimationCurves.easeInOut,
),
);
_controller.repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
void deactivate() {
// Pause shimmer during deactivation to avoid rebuilds in wrong build scope
_controller.stop();
super.deactivate();
}
@override
void activate() {
super.activate();
if (!_controller.isAnimating) {
_controller.repeat();
}
}
@override
Widget build(BuildContext context) {
return Container(
width: widget.width,
height: widget.height,
decoration: BoxDecoration(
borderRadius: widget.borderRadius,
color: context.conduitTheme.shimmerBase,
),
child: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Container(
decoration: BoxDecoration(
borderRadius: widget.borderRadius,
gradient: LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [
Colors.transparent,
context.conduitTheme.shimmerHighlight,
Colors.transparent,
],
stops: [
(_animation.value - 0.3).clamp(0.0, 1.0),
_animation.value.clamp(0.0, 1.0),
(_animation.value + 0.3).clamp(0.0, 1.0),
],
),
),
);
},
),
);
}
}
class _ListItemSkeleton extends StatelessWidget {
final bool showAvatar;
final int lines;
const _ListItemSkeleton({required this.showAvatar, required this.lines});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: Spacing.md,
vertical: Spacing.xs,
),
child: Row(
children: [
if (showAvatar) ...[
ConduitLoading.skeleton(
width: TouchTarget.minimum,
height: TouchTarget.minimum,
borderRadius: BorderRadius.circular(AppBorderRadius.xl),
),
const SizedBox(width: Spacing.xs),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: List.generate(lines, (index) {
return Padding(
padding: EdgeInsets.only(
bottom: index < lines - 1 ? Spacing.sm : 0,
),
child: ConduitLoading.skeleton(
width: index == lines - 1 ? 150 : double.infinity,
height: index == 0 ? 16 : 14,
),
);
}),
),
),
],
),
);
}
}
/// Loading state wrapper for async operations
class LoadingStateWrapper<T> extends StatelessWidget {
final AsyncValue<T> asyncValue;
final Widget Function(T data) builder;
final Widget? loadingWidget;
final Widget Function(Object error, StackTrace stackTrace)? errorBuilder;
final bool showLoadingOverlay;
const LoadingStateWrapper({
super.key,
required this.asyncValue,
required this.builder,
this.loadingWidget,
this.errorBuilder,
this.showLoadingOverlay = false,
});
@override
Widget build(BuildContext context) {
return asyncValue.when(
data: builder,
loading: () => showLoadingOverlay
? ConduitLoading.overlay(
message: AppLocalizations.of(context)!.loadingContent,
)
: loadingWidget ??
ConduitLoading.primary(
message: AppLocalizations.of(context)!.loadingContent,
),
error: (error, stackTrace) {
if (errorBuilder != null) {
return errorBuilder!(error, stackTrace);
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Platform.isIOS
? CupertinoIcons.exclamationmark_triangle
: Icons.error_outline,
size: IconSize.xxl,
color: context.conduitTheme.error,
),
const SizedBox(height: Spacing.md),
Text(
AppLocalizations.of(context)!.errorMessage,
style: TextStyle(
color: context.conduitTheme.textSecondary,
fontSize: AppTypography.headlineSmall,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: Spacing.sm),
Text(
error.toString(),
style: TextStyle(
color: context.conduitTheme.textSecondary,
fontSize: AppTypography.bodySmall,
),
textAlign: TextAlign.center,
),
],
),
);
},
);
}
}
/// Button with loading state
class LoadingButton extends StatelessWidget {
final VoidCallback? onPressed;
final Widget child;
final bool isLoading;
final bool isPrimary;
const LoadingButton({
super.key,
required this.onPressed,
required this.child,
this.isLoading = false,
this.isPrimary = true,
});
@override
Widget build(BuildContext context) {
return FilledButton(
onPressed: isLoading ? null : onPressed,
style: isPrimary
? FilledButton.styleFrom(
backgroundColor: context.conduitTheme.buttonPrimary,
foregroundColor: context.conduitTheme.buttonPrimaryText,
)
: null,
child: isLoading ? ConduitLoading.button(context: context) : child,
);
}
}
/// Refresh indicator with Conduit styling
class ConduitRefreshIndicator extends StatelessWidget {
final Widget child;
final Future<void> Function() onRefresh;
const ConduitRefreshIndicator({
super.key,
required this.child,
required this.onRefresh,
});
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: onRefresh,
color: context.conduitTheme.buttonPrimary,
backgroundColor: context.conduitTheme.surfaceBackground,
child: child,
);
}
}