بناء مكتبة ودجات قابلة لإعادة الاستخدام
مشروع التخرج: مكتبة الودجات الخاصة بك
في درس التخرج هذا، ستجمع كل ما تعلمته عن ودجات Flutter لبناء مكتبة ودجات كاملة وقابلة لإعادة الاستخدام. هذه هي الطريقة التي تعمل بها فرق Flutter المحترفة بالضبط -- تنشئ مكتبة مكونات مشتركة تضمن الاتساق المرئي وتقلل تكرار الكود وتسرّع التطوير عبر التطبيق بأكمله.
سنبني خمسة مكونات جاهزة للإنتاج: AppButton، AppTextField، AppCard، AppAvatar، وAppBadge. كل مكون سيتبع أفضل ممارسات Flutter: تصميم API متسق، تكامل مع التنسيق، دعم إمكانية الوصول، وتوثيق شامل.
- الاتساق -- كل زر وبطاقة وإدخال يبدو ويتصرف بنفس الطريقة عبر التطبيق بأكمله.
- السرعة -- المطورون يستخدمون مكونات مبنية مسبقاً بدلاً من التنسيق من الصفر كل مرة.
- قابلية الصيانة -- غيّر مكوناً في مكان واحد وسيتحدث في كل مكان.
- الجودة -- المكونات تُختبر وتُوثق مرة واحدة، مما يقلل الأخطاء عبر التطبيق.
هيكل المشروع
مكتبة الودجات المنظمة جيداً تتبع هيكل مجلدات واضح. إليك التخطيط الموصى به:
هيكل مجلدات المكتبة
lib/
widgets/
app_button.dart // متغيرات الأزرار
app_text_field.dart // مكون إدخال النص
app_card.dart // متغيرات البطاقات
app_avatar.dart // مكون الصورة الرمزية
app_badge.dart // مكون الشارة/العلامة
widgets.dart // ملف تصدير شامل
theme/
app_theme.dart // تعريفات التنسيق
app_colors.dart // امتدادات الألوان المخصصة
app_spacing.dart // ثوابت المسافات
screens/
demo_screen.dart // عرض المكونات
المكون 1: AppButton
مكون زر متعدد الاستخدامات يدعم متغيرات متعددة (ممتلئ، محدد الخطوط، نص)، أحجام، حالات تحميل، وأيقونات. هذا عادة المكون الأكثر استخداماً في أي تطبيق.
تنفيذ AppButton
import 'package:flutter/material.dart';
/// النمط المرئي للزر.
enum AppButtonVariant { filled, outlined, text, tonal }
/// حجم الزر المحدد مسبقاً.
enum AppButtonSize { small, medium, large }
/// مكون زر قابل للتخصيص يتكامل مع تنسيق التطبيق.
///
/// يدعم أربعة متغيرات (ممتلئ، محدد، نص، درجي)،
/// ثلاثة أحجام، حالة تحميل، أيقونات أمامية/خلفية،
/// ووضع العرض الكامل.
class AppButton extends StatelessWidget {
final String label;
final VoidCallback? onPressed;
final AppButtonVariant variant;
final AppButtonSize size;
final bool isLoading;
final bool isFullWidth;
final IconData? leadingIcon;
final IconData? trailingIcon;
const AppButton({
required this.label,
this.onPressed,
this.variant = AppButtonVariant.filled,
this.size = AppButtonSize.medium,
this.isLoading = false,
this.isFullWidth = false,
this.leadingIcon,
this.trailingIcon,
super.key,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final dimensions = _getDimensions();
// تعطيل الزر أثناء التحميل
final effectiveOnPressed = isLoading ? null : onPressed;
// بناء محتوى الابن
Widget child = _buildContent(theme, dimensions);
// تطبيق قيد العرض الكامل إذا لزم الأمر
if (isFullWidth) {
child = SizedBox(
width: double.infinity,
height: dimensions.height,
child: child,
);
}
return child;
}
Widget _buildContent(ThemeData theme, _ButtonDimensions dims) {
// بناء التسمية مع الأيقونات الاختيارية
Widget label = Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isLoading) ...[
SizedBox(
width: dims.iconSize,
height: dims.iconSize,
child: CircularProgressIndicator(
strokeWidth: 2,
color: _getProgressColor(theme),
),
),
SizedBox(width: dims.iconSpacing),
] else if (leadingIcon != null) ...[
Icon(leadingIcon, size: dims.iconSize),
SizedBox(width: dims.iconSpacing),
],
Text(this.label),
if (trailingIcon != null && !isLoading) ...[
SizedBox(width: dims.iconSpacing),
Icon(trailingIcon, size: dims.iconSize),
],
],
);
final effectiveOnPressed = isLoading ? null : onPressed;
final style = _getStyle(theme, dims);
return switch (variant) {
AppButtonVariant.filled => FilledButton(
onPressed: effectiveOnPressed,
style: style,
child: label,
),
AppButtonVariant.outlined => OutlinedButton(
onPressed: effectiveOnPressed,
style: style,
child: label,
),
AppButtonVariant.text => TextButton(
onPressed: effectiveOnPressed,
style: style,
child: label,
),
AppButtonVariant.tonal => FilledButton.tonal(
onPressed: effectiveOnPressed,
style: style,
child: label,
),
};
}
ButtonStyle _getStyle(ThemeData theme, _ButtonDimensions dims) {
return ButtonStyle(
padding: WidgetStatePropertyAll(dims.padding),
textStyle: WidgetStatePropertyAll(
TextStyle(fontSize: dims.fontSize, fontWeight: FontWeight.w600),
),
minimumSize: WidgetStatePropertyAll(
Size(dims.minWidth, dims.height),
),
shape: WidgetStatePropertyAll(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(dims.borderRadius),
),
),
);
}
Color _getProgressColor(ThemeData theme) {
return switch (variant) {
AppButtonVariant.filled => theme.colorScheme.onPrimary,
AppButtonVariant.outlined => theme.colorScheme.primary,
AppButtonVariant.text => theme.colorScheme.primary,
AppButtonVariant.tonal => theme.colorScheme.onSecondaryContainer,
};
}
_ButtonDimensions _getDimensions() {
return switch (size) {
AppButtonSize.small => const _ButtonDimensions(
height: 36, minWidth: 64,
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
fontSize: 13, iconSize: 16, iconSpacing: 6, borderRadius: 8,
),
AppButtonSize.medium => const _ButtonDimensions(
height: 44, minWidth: 80,
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10),
fontSize: 14, iconSize: 18, iconSpacing: 8, borderRadius: 10,
),
AppButtonSize.large => const _ButtonDimensions(
height: 52, minWidth: 96,
padding: EdgeInsets.symmetric(horizontal: 28, vertical: 14),
fontSize: 16, iconSize: 20, iconSpacing: 10, borderRadius: 12,
),
};
}
}
class _ButtonDimensions {
final double height;
final double minWidth;
final EdgeInsets padding;
final double fontSize;
final double iconSize;
final double iconSpacing;
final double borderRadius;
const _ButtonDimensions({
required this.height, required this.minWidth,
required this.padding, required this.fontSize,
required this.iconSize, required this.iconSpacing,
required this.borderRadius,
});
}
AppButton(label: 'اضغطني', onPressed: ...).المكون 2: AppTextField
إدخال نص منسق يوحّد المظهر عبر التطبيق ويضيف ميزات شائعة مثل تبديل كلمة المرور وعداد الأحرف.
تنفيذ AppTextField
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
/// مكون إدخال نص منسق مع ميزات مدمجة.
///
/// يوفر تنسيقاً متسقاً، تبديل رؤية كلمة المرور اختيارياً،
/// عداد أحرف، أيقونات بادئة/لاحقة، وتكامل التحقق.
class AppTextField extends StatefulWidget {
final String? label;
final String? hint;
final String? helperText;
final TextEditingController? controller;
final String? Function(String?)? validator;
final ValueChanged<String>? onChanged;
final VoidCallback? onEditingComplete;
final TextInputType? keyboardType;
final TextInputAction? textInputAction;
final bool isPassword;
final bool readOnly;
final bool enabled;
final int maxLines;
final int? maxLength;
final IconData? prefixIcon;
final Widget? suffix;
final List<TextInputFormatter>? inputFormatters;
final FocusNode? focusNode;
final bool autofocus;
const AppTextField({
this.label, this.hint, this.helperText,
this.controller, this.validator, this.onChanged,
this.onEditingComplete, this.keyboardType, this.textInputAction,
this.isPassword = false, this.readOnly = false, this.enabled = true,
this.maxLines = 1, this.maxLength, this.prefixIcon, this.suffix,
this.inputFormatters, this.focusNode, this.autofocus = false,
super.key,
});
@override
State<AppTextField> createState() => _AppTextFieldState();
}
class _AppTextFieldState extends State<AppTextField> {
bool _obscureText = true;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return TextFormField(
controller: widget.controller,
validator: widget.validator,
onChanged: widget.onChanged,
onEditingComplete: widget.onEditingComplete,
keyboardType: widget.keyboardType,
textInputAction: widget.textInputAction,
obscureText: widget.isPassword && _obscureText,
readOnly: widget.readOnly,
enabled: widget.enabled,
maxLines: widget.isPassword ? 1 : widget.maxLines,
maxLength: widget.maxLength,
inputFormatters: widget.inputFormatters,
focusNode: widget.focusNode,
autofocus: widget.autofocus,
style: theme.textTheme.bodyLarge,
decoration: InputDecoration(
labelText: widget.label,
hintText: widget.hint,
helperText: widget.helperText,
helperMaxLines: 2,
prefixIcon: widget.prefixIcon != null
? Icon(widget.prefixIcon) : null,
suffixIcon: widget.isPassword
? IconButton(
icon: Icon(_obscureText ? Icons.visibility_off : Icons.visibility),
onPressed: () => setState(() => _obscureText = !_obscureText),
tooltip: _obscureText ? 'إظهار كلمة المرور' : 'إخفاء كلمة المرور',
)
: widget.suffix,
counterText: widget.maxLength != null ? null : '',
),
);
}
}
المكون 3: AppCard
مكون بطاقة مرن مع متغيرات تخطيط متعددة لأنواع محتوى مختلفة.
تنفيذ AppCard
import 'package:flutter/material.dart';
/// متغيرات تخطيط البطاقة.
enum AppCardVariant { standard, outlined, elevated, filled }
/// مكون بطاقة منسق مع تنسيق متسق.
class AppCard extends StatelessWidget {
final Widget child;
final AppCardVariant variant;
final AppCardHeader? header;
final VoidCallback? onTap;
final VoidCallback? onLongPress;
final EdgeInsets? padding;
final EdgeInsets? margin;
final double? width;
final double? height;
const AppCard({
required this.child,
this.variant = AppCardVariant.standard,
this.header, this.onTap, this.onLongPress,
this.padding, this.margin, this.width, this.height,
super.key,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final cardDecoration = switch (variant) {
AppCardVariant.standard => BoxDecoration(
color: colorScheme.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [BoxShadow(
color: colorScheme.shadow.withOpacity(0.08),
blurRadius: 4, offset: const Offset(0, 1),
)],
),
AppCardVariant.outlined => BoxDecoration(
color: colorScheme.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: colorScheme.outlineVariant),
),
AppCardVariant.elevated => BoxDecoration(
color: colorScheme.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [BoxShadow(
color: colorScheme.shadow.withOpacity(0.15),
blurRadius: 12, offset: const Offset(0, 4),
)],
),
AppCardVariant.filled => BoxDecoration(
color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
};
Widget content = Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
if (header != null) ...[header!, const Divider(height: 1)],
Padding(
padding: padding ?? const EdgeInsets.all(16),
child: child,
),
],
);
Widget card = Container(
width: width, height: height,
margin: margin ?? const EdgeInsets.symmetric(vertical: 4),
decoration: cardDecoration,
clipBehavior: Clip.antiAlias,
child: content,
);
if (onTap != null || onLongPress != null) {
card = Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap, onLongPress: onLongPress,
borderRadius: BorderRadius.circular(12),
child: card,
),
);
}
return card;
}
}
/// قسم الرأس لـ AppCard.
class AppCardHeader extends StatelessWidget {
final String title;
final String? subtitle;
final Widget? leading;
final Widget? trailing;
const AppCardHeader({
required this.title, this.subtitle, this.leading, this.trailing,
super.key,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 8, 12),
child: Row(
children: [
if (leading != null) ...[leading!, const SizedBox(width: 12)],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: theme.textTheme.titleMedium),
if (subtitle != null)
Text(subtitle!, style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant)),
],
),
),
if (trailing != null) trailing!,
],
),
);
}
}
المكون 4: AppAvatar
مكون صورة رمزية يتعامل مع الصور، الأحرف الأولى كبديل، مؤشرات حالة الاتصال، وأحجام مختلفة.
تنفيذ AppAvatar
import 'package:flutter/material.dart';
/// أحجام محددة مسبقاً للصورة الرمزية.
enum AppAvatarSize { xs, sm, md, lg, xl }
/// مكون صورة رمزية منسق مع صورة، بديل أحرف أولى،
/// ومؤشر حالة اختياري.
class AppAvatar extends StatelessWidget {
final String? imageUrl;
final String name;
final AppAvatarSize size;
final bool showStatus;
final bool isOnline;
final VoidCallback? onTap;
const AppAvatar({
required this.name,
this.imageUrl, this.size = AppAvatarSize.md,
this.showStatus = false, this.isOnline = false, this.onTap,
super.key,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final dims = _getDimensions();
Widget avatar = Container(
width: dims.size, height: dims.size,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _getBackgroundColor(theme),
),
clipBehavior: Clip.antiAlias,
child: imageUrl != null && imageUrl!.isNotEmpty
? Image.network(imageUrl!, fit: BoxFit.cover,
width: dims.size, height: dims.size,
errorBuilder: (_, __, ___) => _buildInitials(theme, dims))
: _buildInitials(theme, dims),
);
if (showStatus) {
avatar = Stack(
children: [
avatar,
Positioned(
right: 0, bottom: 0,
child: Container(
width: dims.statusSize, height: dims.statusSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isOnline ? const Color(0xFF4CAF50)
: theme.colorScheme.outlineVariant,
border: Border.all(
color: theme.colorScheme.surface,
width: dims.statusBorder),
),
),
),
],
);
}
if (onTap != null) {
avatar = GestureDetector(onTap: onTap, child: avatar);
}
return avatar;
}
Widget _buildInitials(ThemeData theme, _AvatarDimensions dims) {
return Center(
child: Text(_getInitials(),
style: TextStyle(color: Colors.white,
fontSize: dims.fontSize, fontWeight: FontWeight.w600)),
);
}
String _getInitials() {
final parts = name.trim().split(RegExp(r'\s+'));
if (parts.length >= 2) {
return '\${parts.first[0]}\${parts.last[0]}'.toUpperCase();
}
return parts.first.isNotEmpty ? parts.first[0].toUpperCase() : '?';
}
Color _getBackgroundColor(ThemeData theme) {
final colors = [
const Color(0xFF1E88E5), const Color(0xFF43A047),
const Color(0xFFE53935), const Color(0xFF8E24AA),
const Color(0xFFFF6F00), const Color(0xFF00ACC1),
const Color(0xFF5E35B1), const Color(0xFFD81B60),
];
return colors[name.hashCode.abs() % colors.length];
}
_AvatarDimensions _getDimensions() {
return switch (size) {
AppAvatarSize.xs => const _AvatarDimensions(
size: 24, fontSize: 10, statusSize: 8, statusBorder: 1.5),
AppAvatarSize.sm => const _AvatarDimensions(
size: 32, fontSize: 12, statusSize: 10, statusBorder: 2),
AppAvatarSize.md => const _AvatarDimensions(
size: 44, fontSize: 16, statusSize: 12, statusBorder: 2),
AppAvatarSize.lg => const _AvatarDimensions(
size: 64, fontSize: 22, statusSize: 14, statusBorder: 2.5),
AppAvatarSize.xl => const _AvatarDimensions(
size: 96, fontSize: 32, statusSize: 18, statusBorder: 3),
};
}
}
class _AvatarDimensions {
final double size; final double fontSize;
final double statusSize; final double statusBorder;
const _AvatarDimensions({
required this.size, required this.fontSize,
required this.statusSize, required this.statusBorder});
}
المكون 5: AppBadge
مكون شارة/علامة للتسميات والحالات والفئات وعدد الإشعارات.
تنفيذ AppBadge
import 'package:flutter/material.dart';
/// متغيرات لون الشارة.
enum AppBadgeVariant { primary, secondary, success, warning, error, info }
/// أحجام الشارة المحددة مسبقاً.
enum AppBadgeSize { small, medium, large }
/// مكون شارة/علامة منسق لمؤشرات التسميات والحالة.
class AppBadge extends StatelessWidget {
final String label;
final AppBadgeVariant variant;
final AppBadgeSize size;
final IconData? icon;
final bool isDismissible;
final VoidCallback? onDismiss;
const AppBadge({
required this.label,
this.variant = AppBadgeVariant.primary,
this.size = AppBadgeSize.medium,
this.icon, this.isDismissible = false, this.onDismiss,
super.key,
});
/// ينشئ شارة عدد مدمجة (مثل عدد الإشعارات).
factory AppBadge.count({
required int count,
AppBadgeVariant variant = AppBadgeVariant.error,
Key? key,
}) {
return AppBadge(
key: key,
label: count > 99 ? '99+' : count.toString(),
variant: variant,
size: AppBadgeSize.small,
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final dims = _getDimensions();
final colors = _getColors(theme);
return Container(
padding: dims.padding,
decoration: BoxDecoration(
color: colors.background,
borderRadius: BorderRadius.circular(dims.borderRadius),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Icon(icon, size: dims.iconSize, color: colors.foreground),
SizedBox(width: dims.iconSpacing),
],
Text(label, style: TextStyle(
color: colors.foreground, fontSize: dims.fontSize,
fontWeight: FontWeight.w600, height: 1.2)),
if (isDismissible) ...[
SizedBox(width: dims.iconSpacing),
GestureDetector(
onTap: onDismiss,
child: Icon(Icons.close, size: dims.iconSize,
color: colors.foreground),
),
],
],
),
);
}
_BadgeColors _getColors(ThemeData theme) {
final scheme = theme.colorScheme;
return switch (variant) {
AppBadgeVariant.primary => _BadgeColors(
scheme.primaryContainer, scheme.onPrimaryContainer),
AppBadgeVariant.secondary => _BadgeColors(
scheme.secondaryContainer, scheme.onSecondaryContainer),
AppBadgeVariant.success => _BadgeColors(
const Color(0xFFE8F5E9), const Color(0xFF2E7D32)),
AppBadgeVariant.warning => _BadgeColors(
const Color(0xFFFFF3E0), const Color(0xFFE65100)),
AppBadgeVariant.error => _BadgeColors(
scheme.errorContainer, scheme.onErrorContainer),
AppBadgeVariant.info => _BadgeColors(
const Color(0xFFE3F2FD), const Color(0xFF1565C0)),
};
}
_BadgeDimensions _getDimensions() {
return switch (size) {
AppBadgeSize.small => const _BadgeDimensions(
padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2),
fontSize: 11, iconSize: 12, iconSpacing: 3, borderRadius: 6),
AppBadgeSize.medium => const _BadgeDimensions(
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 4),
fontSize: 12, iconSize: 14, iconSpacing: 4, borderRadius: 8),
AppBadgeSize.large => const _BadgeDimensions(
padding: EdgeInsets.symmetric(horizontal: 14, vertical: 6),
fontSize: 14, iconSize: 16, iconSpacing: 6, borderRadius: 10),
};
}
}
class _BadgeColors {
final Color background; final Color foreground;
const _BadgeColors(this.background, this.foreground);
}
class _BadgeDimensions {
final EdgeInsets padding; final double fontSize;
final double iconSize; final double iconSpacing; final double borderRadius;
const _BadgeDimensions({required this.padding, required this.fontSize,
required this.iconSize, required this.iconSpacing, required this.borderRadius});
}
ملف التصدير الشامل
ملف التصدير الشامل يوفر استيراداً واحداً لجميع الودجات. هذا ضروري لسطح API نظيف.
widgets.dart -- تصدير شامل
/// مكتبة ودجات التطبيق
///
/// مجموعة من مكونات واجهة المستخدم القابلة لإعادة الاستخدام والمنسقة.
///
/// الاستخدام:
/// ```dart
/// import 'package:my_app/widgets/widgets.dart';
/// ```
library widgets;
export 'app_button.dart';
export 'app_text_field.dart';
export 'app_card.dart';
export 'app_avatar.dart';
export 'app_badge.dart';
import 'package:my_app/widgets/widgets.dart'; -- هذا يمنح الوصول إلى جميع المكونات وجميع التعدادات وجميع الأنواع المرتبطة. بدونه، كل مكون سيحتاج عبارة استيراد منفصلة.تكامل التنسيق
لمكون الشارة، الألوان المشفرة للنجاح/التحذير/المعلومات يجب أن تأتي بشكل مثالي من امتداد تنسيق حتى تتكيف مع الوضع الداكن. إليك كيفية ربط المكتبة بالتنسيق:
ربط المكونات بامتدادات التنسيق
// lib/theme/app_colors.dart
class AppSemanticColors extends ThemeExtension<AppSemanticColors> {
final Color successBackground;
final Color successForeground;
final Color warningBackground;
final Color warningForeground;
final Color infoBackground;
final Color infoForeground;
const AppSemanticColors({
required this.successBackground, required this.successForeground,
required this.warningBackground, required this.warningForeground,
required this.infoBackground, required this.infoForeground,
});
static const light = AppSemanticColors(
successBackground: Color(0xFFE8F5E9), successForeground: Color(0xFF2E7D32),
warningBackground: Color(0xFFFFF3E0), warningForeground: Color(0xFFE65100),
infoBackground: Color(0xFFE3F2FD), infoForeground: Color(0xFF1565C0),
);
static const dark = AppSemanticColors(
successBackground: Color(0xFF1B5E20), successForeground: Color(0xFFA5D6A7),
warningBackground: Color(0xFFBF360C), warningForeground: Color(0xFFFFCC80),
infoBackground: Color(0xFF0D47A1), infoForeground: Color(0xFF90CAF9),
);
@override
AppSemanticColors copyWith({
Color? successBackground, Color? successForeground,
Color? warningBackground, Color? warningForeground,
Color? infoBackground, Color? infoForeground,
}) {
return AppSemanticColors(
successBackground: successBackground ?? this.successBackground,
successForeground: successForeground ?? this.successForeground,
warningBackground: warningBackground ?? this.warningBackground,
warningForeground: warningForeground ?? this.warningForeground,
infoBackground: infoBackground ?? this.infoBackground,
infoForeground: infoForeground ?? this.infoForeground,
);
}
@override
AppSemanticColors lerp(AppSemanticColors? other, double t) {
if (other is! AppSemanticColors) return this;
return AppSemanticColors(
successBackground: Color.lerp(successBackground, other.successBackground, t)!,
successForeground: Color.lerp(successForeground, other.successForeground, t)!,
warningBackground: Color.lerp(warningBackground, other.warningBackground, t)!,
warningForeground: Color.lerp(warningForeground, other.warningForeground, t)!,
infoBackground: Color.lerp(infoBackground, other.infoBackground, t)!,
infoForeground: Color.lerp(infoForeground, other.infoForeground, t)!,
);
}
}
تطبيق العرض: استعراض المكتبة
شاشة العرض ضرورية للتطوير والتوثيق. تعرض كل متغير مكون حتى يتمكن المطورون من رؤية ما هو متاح:
شاشة عرض المكونات
import 'package:flutter/material.dart';
import '../widgets/widgets.dart';
class WidgetDemoScreen extends StatefulWidget {
const WidgetDemoScreen({super.key});
@override
State<WidgetDemoScreen> createState() => _WidgetDemoScreenState();
}
class _WidgetDemoScreenState extends State<WidgetDemoScreen> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('عرض مكتبة الودجات')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// === الأزرار ===
_sectionTitle(context, 'الأزرار'),
const SizedBox(height: 8),
Wrap(
spacing: 8, runSpacing: 8,
children: [
AppButton(label: 'ممتلئ', onPressed: () {},
variant: AppButtonVariant.filled),
AppButton(label: 'محدد', onPressed: () {},
variant: AppButtonVariant.outlined),
AppButton(label: 'نص', onPressed: () {},
variant: AppButtonVariant.text),
AppButton(label: 'درجي', onPressed: () {},
variant: AppButtonVariant.tonal),
],
),
const SizedBox(height: 12),
Wrap(
spacing: 8, runSpacing: 8,
children: [
AppButton(label: 'حفظ', onPressed: () {},
leadingIcon: Icons.save),
AppButton(label: 'جاري التحميل...', onPressed: () {},
isLoading: true),
AppButton(label: 'معطّل', onPressed: null),
],
),
const SizedBox(height: 32),
// === حقول النص ===
_sectionTitle(context, 'حقول النص'),
const SizedBox(height: 8),
Form(
key: _formKey,
child: Column(
children: [
AppTextField(label: 'البريد الإلكتروني',
hint: 'you@example.com',
prefixIcon: Icons.email,
controller: _emailController,
keyboardType: TextInputType.emailAddress),
const SizedBox(height: 12),
AppTextField(label: 'كلمة المرور',
prefixIcon: Icons.lock,
controller: _passwordController,
isPassword: true),
const SizedBox(height: 12),
AppButton(label: 'إرسال', isFullWidth: true,
onPressed: () {
if (_formKey.currentState!.validate()) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('النموذج صالح!')));
}
}),
],
),
),
const SizedBox(height: 32),
// === البطاقات ===
_sectionTitle(context, 'البطاقات'),
AppCard(
variant: AppCardVariant.standard,
header: const AppCardHeader(
title: 'بطاقة قياسية',
subtitle: 'نمط البطاقة الافتراضي',
leading: Icon(Icons.credit_card)),
child: const Text('محتوى جسم البطاقة هنا.'),
),
AppCard(
variant: AppCardVariant.outlined,
onTap: () {},
child: const Text('بطاقة محددة قابلة للنقر.'),
),
const SizedBox(height: 32),
// === الصور الرمزية ===
_sectionTitle(context, 'الصور الرمزية'),
const SizedBox(height: 8),
Wrap(
spacing: 12, runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
const AppAvatar(name: 'أحمد علي', size: AppAvatarSize.sm),
const AppAvatar(name: 'سارة محمد', size: AppAvatarSize.md),
const AppAvatar(name: 'خالد حسن', size: AppAvatarSize.lg,
showStatus: true, isOnline: true),
const AppAvatar(name: 'فاطمة أحمد', size: AppAvatarSize.xl,
showStatus: true, isOnline: false),
],
),
const SizedBox(height: 32),
// === الشارات ===
_sectionTitle(context, 'الشارات'),
const SizedBox(height: 8),
Wrap(
spacing: 8, runSpacing: 8,
children: [
const AppBadge(label: 'أساسي',
variant: AppBadgeVariant.primary),
const AppBadge(label: 'منشور',
variant: AppBadgeVariant.success, icon: Icons.check_circle),
const AppBadge(label: 'تحذير',
variant: AppBadgeVariant.warning, icon: Icons.warning),
AppBadge.count(count: 5),
AppBadge.count(count: 150),
],
),
const SizedBox(height: 48),
],
),
),
);
}
Widget _sectionTitle(BuildContext context, String title) {
return Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Text(title, style: Theme.of(context).textTheme.headlineSmall),
);
}
}
أفضل الممارسات لمكتبات الودجات
- استخدم التعدادات لأنواع المتغيرات والأحجام -- لا تستخدم أعلام بوليانية متعددة أبداً.
- اجعل التكوين الأكثر شيوعاً هو الافتراضي حتى يتطلب الاستخدام البسيط معاملات أقل.
- استخدم required فقط للمعاملات التي لا يمكن للمكون أن يعمل بدونها حقاً.
- فضّل التركيب على الوراثة -- الودجات يجب أن تقبل ودجات أبناء ولا ترث من بعضها.
- ضمّن معامل Key? key عبر
super.keyفي كل منشئ ودجت.
- الألوان المشفرة -- استخدم دائماً Theme.of(context) أو امتدادات التنسيق. الألوان المشفرة تكسر الوضع الداكن.
- منشئات const المفقودة -- أضف
constللمنشئات عندما تكون جميع الحقول نهائية وليس لها قيم افتراضية قابلة للتغيير. - تجاهل إمكانية الوصول -- ضمّن semanticLabel وtooltip ونسب تباين كافية.
- التعامل المفرط مع المعاملات -- لا تكشف كل تفصيل داخلي. ابدأ بالحد الأدنى وأضف معاملات حسب الحاجة الفعلية.
- نسيان dispose -- StatefulWidgets التي تنشئ متحكمات أو عقد تركيز يجب أن تتخلص منها.
التوثيق بالتعليقات
كل مكون عام يجب أن يحتوي على تعليقات توثيق Dart (///) تتضمن وصفاً وشرح المعاملات ومثال استخدام:
نمط التوثيق
/// وصف موجز لما تفعله الودجت.
///
/// شرح أكثر تفصيلاً للسلوك والمتغيرات
/// ونقاط التكامل.
///
/// ## المعاملات
/// * [label] -- النص المعروض على الزر.
/// * [variant] -- النمط المرئي (ممتلئ، محدد، نص، درجي).
/// * [size] -- حجم محدد مسبقاً (صغير، متوسط، كبير).
/// * [isLoading] -- يعرض دوّارة ويعطّل التفاعل.
///
/// ## مثال
/// ```dart
/// AppButton(
/// label: 'إرسال',
/// onPressed: () => handleSubmit(),
/// variant: AppButtonVariant.filled,
/// leadingIcon: Icons.check,
/// )
/// ```
class AppButton extends StatelessWidget { ... }
الملخص
- مكتبة الودجات توفر الاتساق والسرعة وقابلية الصيانة والجودة عبر تطبيقك.
- استخدم التعدادات لخيارات المتغيرات/الأحجام والمعاملات المسماة مع قيم افتراضية لـ APIs نظيفة.
- كل مكون يجب أن يتكامل مع Theme.of(context) ويدعم الوضع الداكن عبر امتدادات التنسيق.
- أنشئ ملف تصدير شامل (widgets.dart) لاستيراد واحد لجميع المكونات.
- المكونات يجب أن تدعم حالات التحميل والحالات المعطّلة وإمكانية الوصول من البداية.
- ابنِ شاشة عرض تستعرض كل متغير -- تعمل كتوثيق واختبار انحدار مرئي.
- استخدم التركيب بدلاً من الوراثة: AppCard تقبل ودجت ابن وليس فئة فرعية.
- وثّق كل فئة عامة وتعداد بتعليقات /// مع أمثلة استخدام.
- ابدأ بـ API بالحد الأدنى ووسّع حسب حالات الاستخدام الحقيقية -- تجنب التجريد المبكر.
- استخدم فئات مساعدة خاصة (مثل
_ButtonDimensions) لتنظيم منطق الأبعاد بدون تلويث الـ API العام.