Refactoring
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
|
||||
/// Layout de page unifié pour toutes les features de l'application
|
||||
///
|
||||
/// Fournit une structure cohérente avec :
|
||||
/// - AppBar standardisée avec actions personnalisables
|
||||
/// - Body avec padding et scroll automatique
|
||||
/// - FloatingActionButton optionnel
|
||||
/// - Gestion des états de chargement et d'erreur
|
||||
class UnifiedPageLayout extends StatelessWidget {
|
||||
/// Titre de la page affiché dans l'AppBar
|
||||
final String title;
|
||||
|
||||
/// Sous-titre optionnel affiché sous le titre
|
||||
final String? subtitle;
|
||||
|
||||
/// Icône principale de la page
|
||||
final IconData? icon;
|
||||
|
||||
/// Couleur de l'icône (par défaut : primaryColor)
|
||||
final Color? iconColor;
|
||||
|
||||
/// Actions personnalisées dans l'AppBar
|
||||
final List<Widget>? actions;
|
||||
|
||||
/// Contenu principal de la page
|
||||
final Widget body;
|
||||
|
||||
/// FloatingActionButton optionnel
|
||||
final Widget? floatingActionButton;
|
||||
|
||||
/// Position du FloatingActionButton
|
||||
final FloatingActionButtonLocation? floatingActionButtonLocation;
|
||||
|
||||
/// Indique si la page est en cours de chargement
|
||||
final bool isLoading;
|
||||
|
||||
/// Message d'erreur à afficher
|
||||
final String? errorMessage;
|
||||
|
||||
/// Callback pour rafraîchir la page
|
||||
final VoidCallback? onRefresh;
|
||||
|
||||
/// Padding personnalisé pour le body (par défaut : 16.0)
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
/// Indique si le body doit être scrollable (par défaut : true)
|
||||
final bool scrollable;
|
||||
|
||||
/// Couleur de fond personnalisée
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// Indique si l'AppBar doit être affichée (par défaut : true)
|
||||
final bool showAppBar;
|
||||
|
||||
const UnifiedPageLayout({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.body,
|
||||
this.subtitle,
|
||||
this.icon,
|
||||
this.iconColor,
|
||||
this.actions,
|
||||
this.floatingActionButton,
|
||||
this.floatingActionButtonLocation,
|
||||
this.isLoading = false,
|
||||
this.errorMessage,
|
||||
this.onRefresh,
|
||||
this.padding,
|
||||
this.scrollable = true,
|
||||
this.backgroundColor,
|
||||
this.showAppBar = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: backgroundColor ?? AppTheme.backgroundLight,
|
||||
appBar: showAppBar ? _buildAppBar(context) : null,
|
||||
body: _buildBody(context),
|
||||
floatingActionButton: floatingActionButton,
|
||||
floatingActionButtonLocation: floatingActionButtonLocation,
|
||||
);
|
||||
}
|
||||
|
||||
PreferredSizeWidget _buildAppBar(BuildContext context) {
|
||||
return AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
scrolledUnderElevation: 1,
|
||||
surfaceTintColor: Colors.white,
|
||||
title: Row(
|
||||
children: [
|
||||
if (icon != null) ...[
|
||||
Icon(
|
||||
icon,
|
||||
color: iconColor ?? AppTheme.primaryColor,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
],
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
if (subtitle != null)
|
||||
Text(
|
||||
subtitle!,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: actions,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody(BuildContext context) {
|
||||
Widget content = body;
|
||||
|
||||
// Gestion des états d'erreur
|
||||
if (errorMessage != null) {
|
||||
content = _buildErrorState(context);
|
||||
}
|
||||
// Gestion de l'état de chargement
|
||||
else if (isLoading) {
|
||||
content = _buildLoadingState();
|
||||
}
|
||||
|
||||
// Application du padding
|
||||
if (padding != null || (padding == null && scrollable)) {
|
||||
content = Padding(
|
||||
padding: padding ?? const EdgeInsets.all(16.0),
|
||||
child: content,
|
||||
);
|
||||
}
|
||||
|
||||
// Gestion du scroll
|
||||
if (scrollable && errorMessage == null && !isLoading) {
|
||||
if (onRefresh != null) {
|
||||
content = RefreshIndicator(
|
||||
onRefresh: () async => onRefresh!(),
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: content,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
content = SingleChildScrollView(child: content);
|
||||
}
|
||||
}
|
||||
|
||||
return SafeArea(child: content);
|
||||
}
|
||||
|
||||
Widget _buildLoadingState() {
|
||||
return const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(AppTheme.primaryColor),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Chargement...',
|
||||
style: TextStyle(
|
||||
color: AppTheme.textSecondary,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorState(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: AppTheme.errorColor,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Une erreur est survenue',
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
errorMessage!,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
if (onRefresh != null)
|
||||
ElevatedButton.icon(
|
||||
onPressed: onRefresh,
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Réessayer'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user