دليل شامل ومزدوج اللغة (عربي / إنجليزي) لبناء Design System احترافي في Flutter باستخدام منهجية Atomic Design.
- Why a Design System? / لماذا نحتاج Design System؟
- Tokens / الرموز الأساسية
- Atomic Design Layers / طبقات Atomic Design
- Theme System / نظام الثيم
- Typography / الطباعة
- Components Deep Dive / تعمق في المكونات
- Assets Management / إدارة الأصول
- Project Structure / هيكل المشروع
- Testing & Widgetbook / الاختبار والكتالوج
- Best Practices & Anti-Patterns / أفضل الممارسات والأخطاء الشائعة
🟢 Foundational / أساسي
🟡 Intermediate / متوسط
🔴 Advanced / متقدم
الخطوة 1: فهم الـ Design System والمشكلة التي يحلها
What it solves:
- Eliminates visual inconsistency across screens and platforms.
- Removes redundant code - one component, used everywhere.
- Enables a single package shared across mobile, web, and desktop.
- Bridges the gap between designers and developers with shared language.
المشكلات التي يحلها:
- يُنهي حالة التضارب البصري بين الشاشات والمنصات المختلفة.
- يُلغي تكرار الكود - مكوّن واحد يُستخدم في كل مكان.
- يتيح مشاركة باكدج واحد بين Mobile وWeb وDesktop.
- يُجسّر الهوّة بين المصمم والمطور بلغة مشتركة وموحدة.
Important
A Design System is a product inside your product - it needs ownership, versioning, and maintenance like any other package.
الـ Design System هو منتج داخل منتجك - يحتاج مالكاً وإصداراً وصيانة مستمرة مثل أي باكدج آخر.
الخطوة 2: استكشاف أساسيات Atomic Design ومن ابتكره
Atomic Design Fundamentals:
- Coined by Brad Frost - inspired by chemistry's atomic structure.
- Breaks UI into 5 hierarchical layers: Tokens → Atoms → Molecules → Organisms → Templates → Pages.
- Forces developers to think in components, not screens.
- Works universally - React, Vue, SwiftUI, and Flutter alike.
أساسيات Atomic Design:
- ابتكره Brad Frost - مستوحى من البنية الذرية في الكيمياء.
- يُقسّم الواجهة إلى 5 طبقات هرمية: Tokens ← Atoms ← Molecules ← Organisms ← Templates ← Pages.
- يُجبر المطور على التفكير بالمكونات لا بالشاشات كاملة.
- يعمل عالمياً - React وVue وSwiftUI وFlutter على حدٍّ سواء.
الخطوة 3: تحديد الفوائد القابلة للقياس من تبني Design System
Measurable Benefits:
- 80% reduction in time-to-market for new screens.
- Zero color or spacing inconsistencies across the app.
- New developer onboarding reduced from days to hours.
- Dark mode support requires changes in one place only.
- QA effort drops - pre-tested atoms mean fewer regressions.
الفوائد القابلة للقياس:
- تخفيض 80% في وقت تسليم الشاشات الجديدة.
- صفر تضارب في الألوان أو المسافات عبر التطبيق.
- تأهيل المطور الجديد يتقلص من أيام إلى ساعات.
- دعم الـ Dark Mode يتطلب تعديلاً في مكان واحد فقط.
- جهد الـ QA ينخفض - atoms مُختبرة مسبقاً = رجعات أقل.
الخطوة 4: تعريف الـ Design Tokens وقواعد تسميتها
Design Tokens Rules:
- Raw primitive values - colors, sizes, radii, shadows, durations.
- Named semantically, not by usage:
brand.500NOTbuttonColor. - Only the design team defines token values - developers consolidate them.
- Changing one token propagates the change to every consumer automatically.
قواعد الـ Design Tokens:
- قيم خام أولية - ألوان وأحجام وزوايا وظلال ومدد زمنية.
- تُسمى دلالياً لا باستخدامها:
brand.500وليسbuttonColor. - فريق التصميم فقط يحدد قيم الـ tokens - المطور يُجمّعها فقط.
- تغيير token واحد ينتشر تلقائياً لكل مكان يستخدمه.
الخطوة 5: تنفيذ AppColors مع دعم كامل للـ palette
AppColors Implementation:
/// {@template app_colors}
/// Colors class for themes - provides direct access via static fields.
/// {@endtemplate}
abstract final class AppColors {
static const white = Colors.white;
static const black = Colors.black;
static const transparent = Colors.transparent;
/// Brand color palette - primary identity color.
/// لوحة ألوان العلامة التجارية - اللون التعريفي الأساسي.
static const brand = MaterialColor(
0xFF347AF6,
{
50: Color(0xFFF0F5FF), // Lightest / الأفتح
100: Color(0xFFE0ECFF),
200: Color(0xFFBDD3F9),
300: Color(0xFF81ACF9),
400: Color(0xFF5A93F9),
500: Color(0xFF347AF6), // Primary / الأساسي
600: Color(0xFF1559D1),
700: Color(0xFF174EAF),
800: Color(0xFF1D4387),
900: Color(0xFF163367), // Darkest / الأغمق
},
);
static const grayLight = MaterialColor(0xFF667085, { ... });
static const error = MaterialColor(0xFFF04438, { ... });
static const success = MaterialColor(0xFF12B76A, { ... });
static const warning = MaterialColor(0xFFF79009, { ... });
}Note
Use MaterialColor for brand palettes - it gives you shade access like AppColors.brand.shade500 and works seamlessly with Flutter's theming.
استخدم MaterialColor للـ palettes - يتيح الوصول كـ AppColors.brand.shade500 ويتكامل مع نظام الثيم في Flutter.
الخطوة 6: تعريف tokens الـ Spacing والـ Radius والـ Shadow
Spacing Tokens:
/// {@template app_spacing}
/// All spacing values used across the app - horizontal and vertical alike.
/// جميع قيم التباعد المستخدمة - أفقية وعمودية على حدٍّ سواء.
/// {@endtemplate}
abstract final class AppSpacing {
static const none = 0.0;
static const xxs = 2.0;
static const xs = 4.0;
static const sm = 6.0;
static const md = 8.0;
static const lg = 12.0;
static const xl = 16.0;
static const xxl = 24.0;
static const xxxl = 32.0;
static const huge = 48.0;
}Radius Tokens:
/// {@template app_radius}
/// Border radius constants - use these instead of raw Radius.circular().
/// ثوابت نصف القطر - استخدمها بدلاً من Radius.circular() المباشرة.
/// {@endtemplate}
abstract final class AppRadius {
static const none = Radius.zero;
static const xxs = Radius.circular(2);
static const xs = Radius.circular(4);
static const sm = Radius.circular(6);
static const md = Radius.circular(8);
static const lg = Radius.circular(12);
static const xl = Radius.circular(16);
static const full = Radius.circular(999); // Pill shape / شكل حبة دواء
}Shadow Tokens:
/// {@template app_shadow}
/// Elevation shadows - defined as List<BoxShadow> for direct use.
/// ظلال الارتفاع - مُعرّفة كـ List<BoxShadow> للاستخدام المباشر.
/// {@endtemplate}
abstract final class AppShadow {
static const none = <BoxShadow>[];
static const xs = [
BoxShadow(
blurRadius: 2,
offset: Offset(0, 1),
color: Color.fromRGBO(16, 24, 40, 0.05),
),
];
static const sm = [
BoxShadow(color: Color(0x0F101828), blurRadius: 2, offset: Offset(0, 1)),
BoxShadow(color: Color(0x19101828), blurRadius: 3, offset: Offset(0, 1)),
];
static const md = [
BoxShadow(color: Color(0x0F101828), blurRadius: 4, offset: Offset(0, 2)),
BoxShadow(color: Color(0x19101828), blurRadius: 8, offset: Offset(0, 4)),
];
static const lg = [
BoxShadow(color: Color(0x0F101828), blurRadius: 8, offset: Offset(0, 4)),
BoxShadow(color: Color(0x19101828), blurRadius: 16, offset: Offset(0, 8)),
];
}Tip
Define shadows as static const List<BoxShadow> so they can be used directly in BoxDecoration.boxShadow without any conversion.
عرّف الظلال كـ static const List<BoxShadow> لاستخدامها مباشرة في BoxDecoration.boxShadow بدون أي تحويل.
الخطوة 7: تفصيل طبقات Atomic Design الخمس ومحتوياتها
Layer Breakdown:
| Layer / الطبقة | Contains / تحتوي | Flutter Equivalent / المقابل في Flutter |
|---|---|---|
| Tokens | Raw values / قيم خام | class AppColors, AppSpacing |
| Atoms | Indivisible widgets | AppButton, AppTextField, AppIcon |
| Molecules | 2+ atoms with one purpose | SearchField, UserCard, FormRow |
| Organisms | Complex, multi-responsibility | LoginForm, AppHeader, ProductList |
| Templates | Layout skeleton, no real data | AuthTemplate, DashboardTemplate |
| Pages | Template + real data + routing | LoginPage, HomeScreen |
تفصيل الطبقات:
Pages ← شاشات كاملة مع بيانات حقيقية
↑
Templates ← هيكل الشاشة بدون بيانات
↑
Organisms ← مكونات معقدة متعددة المسؤوليات
↑
Molecules ← مجموعة Atoms لغرض واحد
↑
Atoms ← أصغر وحدة لا تنقسم
↑
Tokens ← قيم خام (ألوان، مسافات، ظلال)
الخطوة 8: بناء Atom أساسي (نمط الزر المجرد)
Abstract Button Atom:
/// Typedef for icon builder - receives state color, returns Widget.
/// Typedef لبناء الأيقونة - يستقبل لون الحالة، يُعيد Widget.
typedef IconBuilder = Widget Function(Color iconColor);
/// {@template app_text_button}
/// Abstract base for all text buttons - subclass to define colors.
/// قاعدة مجردة لكل أزرار النص - ورّث منها لتحديد الألوان.
/// {@endtemplate}
abstract base class AppTextButton extends StatelessWidget {
const AppTextButton({
super.key,
required this.label,
this.onTap,
this.leading,
this.trailing,
this.appButtonSize = AppButtonSize.medium,
});
final String label;
final VoidCallback? onTap;
final IconBuilder? leading;
final IconBuilder? trailing;
final AppButtonSize appButtonSize;
// Subclasses define these / الأبناء يعرّفون هذه
Color backgroundColor(BuildContext context);
Color hoverColor(BuildContext context);
Color focusColor(BuildContext context);
Color disabledColor(BuildContext context);
Color textColor(BuildContext context);
Color disabledTextColor(BuildContext context) =>
context.buttonTheme.primaryTextDisabled;
// Border overrides - default to none / افتراضياً بدون حدود
BorderSide defaultBorder(BuildContext context) => BorderSide.none;
BorderSide focusedBorder(BuildContext context) => BorderSide.none;
BorderSide hoverBorder(BuildContext context) => BorderSide.none;
BorderSide disabledBorder(BuildContext context) => BorderSide.none;
@override
Widget build(BuildContext context) { ... }
}
// Concrete implementations / التطبيقات الملموسة
class PrimaryTextButton extends AppTextButton {
const PrimaryTextButton({super.key, required super.label, super.onTap, super.appButtonSize});
@override Color backgroundColor(BuildContext ctx) => ctx.buttonTheme.primaryDefault;
@override Color hoverColor(BuildContext ctx) => ctx.buttonTheme.primaryHover;
@override Color focusColor(BuildContext ctx) => ctx.buttonTheme.primaryFocused;
@override Color disabledColor(BuildContext ctx) => ctx.buttonTheme.primaryDisabled;
@override Color textColor(BuildContext ctx) => ctx.buttonTheme.primaryText;
}
class SecondaryTextButton extends AppTextButton { ... }
class OutlineTextButton extends AppTextButton { ... }
class GhostTextButton extends AppTextButton { ... }Button Size Enum:
/// Enum for button sizes - maps to fixed heights.
/// Enum لأحجام الأزرار - يُعيّن ارتفاعات ثابتة.
enum AppButtonSize {
xSmall, // height: 36 - padding: H:12
small, // height: 36 - padding: H:12
medium, // height: 40 - padding: H:16
large, // height: 44 - padding: H:16
xlarge, // height: 48 - padding: H:20
xxLarge, // height: 56 - padding: H:24
}Important
Never hardcode colors inside a widget - always delegate to the abstract method which reads from context.buttonTheme. This is what makes theming and dark mode trivial.
لا تضع ألواناً صلبة داخل أي widget - فوّض دائماً للـ abstract method التي تقرأ من context.buttonTheme. هذا ما يجعل الـ theming والـ dark mode أمراً تافهاً.
الخطوة 9: بناء Molecule (مثال SearchField)
SearchField Molecule:
/// {@template search_field}
/// A search input molecule - combines [AppTextField] + search [AppIcon].
/// مولكيول حقل البحث - يجمع [AppTextField] مع أيقونة البحث [AppIcon].
/// {@endtemplate}
class SearchField extends StatelessWidget {
const SearchField({
super.key,
this.onChanged,
this.onSubmit,
this.hintText = 'Search...',
});
final ValueChanged<String>? onChanged;
final VoidCallback? onSubmit;
final String hintText;
@override
Widget build(BuildContext context) {
return AppTextField( // Atom / ذرة
labelText: hintText,
onChanged: onChanged,
keyboardType: TextInputType.text,
onEditingComplete: onSubmit,
suffixIcon: GestureDetector( // Atom / ذرة
onTap: onSubmit,
child: SvgPicture(loader: AppAssets.searchIcon),
),
);
}
}Note
A Molecule has one responsibility. If your molecule is doing two unrelated things, it should be an Organism.
المولكيول لها مسؤولية واحدة. إذا كانت تفعل شيئين غير مترابطين، فهي Organism.
الخطوة 10: بناء Organism (مثال LoginForm)
LoginForm Organism:
/// {@template login_form}
/// Login form organism - orchestrates multiple atoms and molecules.
/// كائن نموذج تسجيل الدخول - ينسّق atoms و molecules متعددة.
/// {@endtemplate}
class LoginForm extends StatefulWidget {
const LoginForm({super.key, required this.onSubmit});
final void Function(String email, String password) onSubmit;
@override
State<LoginForm> createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
final _formKey = GlobalKey<FormState>();
final _email = TextEditingController();
final _password = TextEditingController();
bool _obscure = true;
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Atom / ذرة
AppTextField(
controller: _email,
labelText: 'Email',
keyboardType: TextInputType.emailAddress,
validator: (v) => v!.contains('@') ? null : 'Invalid email',
),
const SizedBox(height: AppSpacing.xl),
// Atom / ذرة
AppTextField(
controller: _password,
labelText: 'Password',
obscureText: _obscure,
suffixIcon: IconButton(
icon: Icon(_obscure ? Icons.visibility : Icons.visibility_off),
onPressed: () => setState(() => _obscure = !_obscure),
),
validator: (v) => v!.length >= 8 ? null : 'Min 8 characters',
),
const SizedBox(height: AppSpacing.xxl),
// Atom / ذرة
PrimaryTextButton(
label: 'Log In',
appButtonSize: AppButtonSize.large,
onTap: () {
if (_formKey.currentState!.validate()) {
widget.onSubmit(_email.text, _password.text);
}
},
),
],
),
);
}
@override
void dispose() {
_email.dispose();
_password.dispose();
super.dispose();
}
}الخطوة 11: التفريق بين Template و Page في التطبيق الفعلي
Template - Layout only, no data:
/// {@template auth_template}
/// Auth layout template - provides consistent padding and background.
/// قالب تخطيط المصادقة - يوفر padding وخلفية متسقة.
/// {@endtemplate}
class AuthTemplate extends StatelessWidget {
const AuthTemplate({super.key, required this.body, this.header});
final Widget body;
final Widget? header;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: context.layoutTheme.authBackground,
body: SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.xxl,
vertical: AppSpacing.xl,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (header != null) ...[header!, const SizedBox(height: AppSpacing.xxl)],
Expanded(child: body),
],
),
),
),
);
}
}Page - Template + real data + routing:
/// {@template login_page}
/// Login page - wires template, organism, and state management.
/// صفحة تسجيل الدخول - تربط القالب والكائن وإدارة الحالة.
/// {@endtemplate}
class LoginPage extends StatelessWidget {
const LoginPage({super.key});
@override
Widget build(BuildContext context) {
return AuthTemplate( // Template / قالب
header: AppAssetImage( // Atom / ذرة
path: AppAssets.logoFull,
),
body: LoginForm( // Organism / كائن
onSubmit: (email, password) {
context.read<AuthCubit>() // State management
.login(email, password);
},
),
);
}
}Tip
Pages are the only layer that knows about state management (BLoC, Riverpod, etc.) and routing. Templates and below are completely framework-agnostic.
الـ Pages هي الطبقة الوحيدة التي تعرف عن إدارة الحالة والـ routing. كل ما دونها مستقل تماماً عن أي framework.
الخطوة 12: بناء ThemeExtension على مستوى المكوّن
AppButtonTheme with ThemeExtension:
/// {@template app_button_theme}
/// Theming contract for all button variants.
/// عقد الـ theming لكل متغيرات الأزرار.
/// {@endtemplate}
class AppButtonTheme extends ThemeExtension<AppButtonTheme> {
const AppButtonTheme({
required this.primaryText,
required this.primaryDefault,
required this.primaryHover,
required this.primaryFocused,
required this.primaryDisabled,
required this.primaryTextDisabled,
required this.secondaryDefault,
required this.secondaryHover,
required this.outlinedDefault,
required this.outlinedBorderDisabled,
required this.buttonLineDefault,
});
/// Light mode factory / مصنع الوضع الفاتح
factory AppButtonTheme.light() => AppButtonTheme(
primaryText: AppColors.white,
primaryDefault: AppColors.brand.shade500,
primaryHover: AppColors.brand.shade600,
primaryFocused: AppColors.brand.shade700,
primaryDisabled: AppColors.grayLight.shade200,
primaryTextDisabled: AppColors.grayLight.shade300,
secondaryDefault: AppColors.grayLight.shade100,
secondaryHover: AppColors.grayLight.shade150,
outlinedDefault: AppColors.white,
outlinedBorderDisabled: AppColors.grayLight.shade200,
buttonLineDefault: AppColors.grayLight.shade300,
);
/// Dark mode factory / مصنع الوضع المظلم
factory AppButtonTheme.dark() => AppButtonTheme(
primaryText: AppColors.white,
primaryDefault: AppColors.brand.shade400,
primaryHover: AppColors.brand.shade300,
primaryFocused: AppColors.brand.shade200,
primaryDisabled: AppColors.grayDark.shade700,
primaryTextDisabled: AppColors.grayDark.shade500,
secondaryDefault: AppColors.grayDark.shade800,
secondaryHover: AppColors.grayDark.shade700,
outlinedDefault: AppColors.transparent,
outlinedBorderDisabled: AppColors.grayDark.shade700,
buttonLineDefault: AppColors.grayDark.shade500,
);
final Color primaryText;
final Color primaryDefault;
final Color primaryHover;
final Color primaryFocused;
final Color primaryDisabled;
final Color primaryTextDisabled;
final Color secondaryDefault;
final Color secondaryHover;
final Color outlinedDefault;
final Color outlinedBorderDisabled;
final Color buttonLineDefault;
@override
AppButtonTheme copyWith({ Color? primaryDefault, ... }) => AppButtonTheme(
primaryDefault: primaryDefault ?? this.primaryDefault,
// ...
);
@override
AppButtonTheme lerp(covariant AppButtonTheme? other, double t) {
if (other is! AppButtonTheme) return this;
return AppButtonTheme(
primaryDefault: Color.lerp(primaryDefault, other.primaryDefault, t)!,
// lerp all fields / حوّل كل الحقول
);
}
}الخطوة 13: تجميع كل الـ themes في AppTheme واحد
AppTheme Aggregator:
/// {@template app_theme}
/// Single entry point for all design system themes.
/// نقطة دخول واحدة لكل themes نظام التصميم.
/// {@endtemplate}
class AppTheme extends ThemeExtension<AppTheme> {
const AppTheme({
required this.appButtonTheme,
required this.appInputTheme,
required this.appTypography,
required this.appCheckboxTheme,
required this.appToggleTheme,
required this.appBadgeTheme,
required this.appAvatarTheme,
required this.appNavigationTheme,
required this.appLayoutTheme,
});
factory AppTheme.light() => AppTheme(
appButtonTheme: AppButtonTheme.light(),
appInputTheme: AppInputTheme.light(),
appTypography: AppRegularTypography(),
appCheckboxTheme: AppCheckboxTheme.light(),
appToggleTheme: AppToggleTheme.light(),
appBadgeTheme: AppBadgeTheme.light(),
appAvatarTheme: AppAvatarTheme.light(),
appNavigationTheme: AppNavigationTheme.light(),
appLayoutTheme: AppLayoutTheme.light(),
);
factory AppTheme.dark() => AppTheme(
appButtonTheme: AppButtonTheme.dark(),
appInputTheme: AppInputTheme.dark(),
appTypography: AppRegularTypography(),
// ...
);
final ThemeExtension<AppButtonTheme> appButtonTheme;
final ThemeExtension<AppInputTheme> appInputTheme;
final AppTypography appTypography;
// ...
@override AppTheme copyWith({ ... }) => ...;
@override AppTheme lerp(covariant AppTheme? other, double t) => ...;
}الخطوة 14: عرض الـ theme عبر extensions على BuildContext
BuildContext Theme Extensions:
/// Extensions on [BuildContext] - clean theme access everywhere.
/// Extensions على [BuildContext] - وصول نظيف للـ theme في كل مكان.
extension ThemeExt on BuildContext {
ThemeData get theme => Theme.of(this);
AppTheme get _appTheme => theme.extension<AppTheme>()!;
AppButtonTheme get buttonTheme => _appTheme.appButtonTheme as AppButtonTheme;
AppInputTheme get inputTheme => _appTheme.appInputTheme as AppInputTheme;
AppTypography get typography => _appTheme.appTypography as AppTypography;
AppCheckboxTheme get checkboxTheme => _appTheme.appCheckboxTheme as AppCheckboxTheme;
AppToggleTheme get toggleTheme => _appTheme.appToggleTheme as AppToggleTheme;
AppBadgeTheme get badgeTheme => _appTheme.appBadgeTheme as AppBadgeTheme;
AppAvatarTheme get avatarTheme => _appTheme.appAvatarTheme as AppAvatarTheme;
AppNavigationTheme get navigationTheme => _appTheme.appNavigationTheme as AppNavigationTheme;
AppLayoutTheme get layoutTheme => _appTheme.appLayoutTheme as AppLayoutTheme;
}
// Usage / الاستخدام:
context.buttonTheme.primaryDefault
context.typography.titleSmall
context.inputTheme.borderFocused
context.badgeTheme.successBackgroundالخطوة 15: توفير الثيم وتبديله باستخدام ThemeScope
ThemeScope InheritedWidget:
/// {@template theme_scope}
/// InheritedWidget - provides [AppTheme] to the entire widget tree.
/// InheritedWidget - يُوفّر [AppTheme] لكل شجرة الـ widgets.
/// {@endtemplate}
class ThemeScope extends InheritedWidget {
const ThemeScope({
super.key,
required super.child,
required this.themeMode,
required this.appTheme,
});
final ThemeMode themeMode;
final AppTheme appTheme;
static ThemeScope of(BuildContext context) {
final result = context.dependOnInheritedWidgetOfExactType<ThemeScope>();
assert(result != null, 'ThemeScope not found - wrap your app with ThemeScopeWidget');
return result!;
}
@override
bool updateShouldNotify(ThemeScope old) =>
old.themeMode != themeMode || old.appTheme != appTheme;
}ThemeScopeWidget with persistence:
const _kThemeMode = 'themeMode';
class ThemeScopeWidget extends StatefulWidget {
const ThemeScopeWidget({super.key, required this.child, required this.preferences});
final Widget child;
final SharedPreferences preferences;
/// One-liner initializer for main() / مُهيّئ سطر واحد لـ main()
static Future<ThemeScopeWidget> initialize(Widget child) async {
final prefs = await SharedPreferences.getInstance();
return ThemeScopeWidget(preferences: prefs, child: child);
}
static ThemeScopeWidgetState? of(BuildContext context) =>
context.findRootAncestorStateOfType<ThemeScopeWidgetState>();
@override
State<ThemeScopeWidget> createState() => ThemeScopeWidgetState();
}
class ThemeScopeWidgetState extends State<ThemeScopeWidget> {
ThemeMode _themeMode = ThemeMode.system;
/// Toggle theme from anywhere / بدّل الثيم من أي مكان
Future<void> changeTo(ThemeMode mode) async {
if (_themeMode == mode) return;
await widget.preferences.setInt(_kThemeMode, ThemeMode.values.indexOf(mode));
setState(() => _themeMode = mode);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final idx = widget.preferences.getInt(_kThemeMode) ?? 0;
_themeMode = ThemeMode.values[idx];
}
@override
Widget build(BuildContext context) {
final brightness = MediaQuery.platformBrightnessOf(context);
final appTheme = switch (_themeMode) {
ThemeMode.light => AppTheme.light(),
ThemeMode.dark => AppTheme.dark(),
ThemeMode.system => brightness == Brightness.dark
? AppTheme.dark()
: AppTheme.light(),
};
return ThemeScope(
themeMode: _themeMode,
appTheme: appTheme,
child: widget.child,
);
}
}main.dart setup / إعداد main.dart:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final app = await ThemeScopeWidget.initialize(const MyApp());
runApp(app);
}
// In MaterialApp / في MaterialApp:
final scope = ThemeScope.of(context);
return MaterialApp(
themeMode: scope.themeMode,
theme: ThemeData(extensions: [scope.appTheme]),
darkTheme: ThemeData(extensions: [scope.appTheme]),
);
// Change theme from any widget / غيّر الثيم من أي widget:
ThemeScopeWidget.of(context)?.changeTo(ThemeMode.dark);Important
The lerp method in ThemeExtension is what enables smooth animated transitions between light and dark mode - never skip it.
دالة lerp في ThemeExtension هي ما يُتيح الانتقال المتحرك السلس بين الوضع الفاتح والمظلم - لا تتخطاها أبداً.
الخطوة 16: تعريف نظام AppTypography قابل للتوسع
AppTypography as ThemeExtension:
/// {@template app_typography}
/// Typography contract - defines all TextStyles as named properties.
/// عقد الطباعة - يُعرّف كل TextStyles كخصائص مسماة.
/// {@endtemplate}
interface class AppTypography extends ThemeExtension<AppTypography> {
AppTypography({
// Display / عرض
required this.display2XLarge,
required this.displayXLarge,
required this.displayLarge,
// Title / عناوين
required this.titleLarge,
required this.titleMedium,
required this.titleSmall,
// Body / نص أساسي
required this.bodyLarge,
required this.bodyMedium,
required this.bodySmall,
// Label / تسميات
required this.labelLarge,
required this.labelMedium,
required this.labelSmall,
// Button / أزرار
required this.button2XLarge,
required this.buttonXLarge,
required this.buttonLarge,
required this.buttonMedium,
required this.buttonSmall,
// Input / مدخلات
required this.inputLabel,
required this.inputPlaceHolder,
required this.inputHint,
});
final TextStyle display2XLarge;
final TextStyle displayXLarge;
final TextStyle displayLarge;
final TextStyle titleLarge;
final TextStyle titleMedium;
final TextStyle titleSmall;
final TextStyle bodyLarge;
final TextStyle bodyMedium;
final TextStyle bodySmall;
final TextStyle labelLarge;
final TextStyle labelMedium;
final TextStyle labelSmall;
final TextStyle button2XLarge;
final TextStyle buttonXLarge;
final TextStyle buttonLarge;
final TextStyle buttonMedium;
final TextStyle buttonSmall;
final TextStyle inputLabel;
final TextStyle inputPlaceHolder;
final TextStyle inputHint;
@override AppTypography copyWith({ ... }) => ...;
@override AppTypography lerp(covariant AppTypography? other, double t) => ...;
}
/// Concrete regular typography implementation.
/// التطبيق الملموس لطباعة النص العادي.
class AppRegularTypography extends AppTypography {
AppRegularTypography({
super.titleLarge = const TextStyle(
fontSize: 20, height: 30 / 20, fontWeight: FontWeight.w600,
),
super.titleMedium = const TextStyle(
fontSize: 18, height: 28 / 18, fontWeight: FontWeight.w600,
),
super.titleSmall = const TextStyle(
fontSize: 16, height: 24 / 16, fontWeight: FontWeight.w600,
),
super.bodyLarge = const TextStyle(
fontSize: 16, height: 24 / 16, fontWeight: FontWeight.w400,
),
super.bodyMedium = const TextStyle(
fontSize: 14, height: 20 / 14, fontWeight: FontWeight.w400,
),
super.buttonLarge = const TextStyle(
fontSize: 16, height: 24 / 16, fontWeight: FontWeight.w500,
),
super.buttonMedium = const TextStyle(
fontSize: 14, height: 20 / 14, fontWeight: FontWeight.w500,
),
// ... all other styles
});
}Tip
Set height as lineHeight / fontSize (e.g. 24 / 16 = 1.5) - this matches Figma's line height exactly and avoids clipping descenders on dense layouts.
اضبط height كـ lineHeight / fontSize (مثل 24 / 16 = 1.5) - يتطابق مع line height في Figma تماماً ويمنع قطع الأحرف السفلية في التخطيطات المكتظة.
الخطوة 17: تنفيذ AppTextField بتصميم مدفوع بالحالة بالكامل
AppTextField with WidgetStateTextStyle:
/// {@template app_text_field}
/// A fully theme-driven text field - all colors from [AppInputTheme].
/// حقل نص مدفوع بالثيم كلياً - كل الألوان من [AppInputTheme].
/// {@endtemplate}
class AppTextField extends StatelessWidget {
const AppTextField({
super.key,
this.controller,
this.labelText,
this.helperText,
this.errorText,
this.enabled = true,
this.obscureText = false,
this.maxLines = 1,
this.onChanged,
this.validator,
this.suffixIcon,
this.prefixIcon,
this.suffixIconConstraints = const BoxConstraints(minHeight: 24, minWidth: 40),
this.prefixIconConstraints = const BoxConstraints(minHeight: 24, minWidth: 40),
this.keyboardType,
this.inputFormatters,
this.autofillHints,
this.onEditingComplete,
this.autovalidateMode = AutovalidateMode.onUserInteraction,
});
final TextEditingController? controller;
final String? labelText;
final String? helperText;
final String? errorText;
final bool enabled;
final bool obscureText;
final int maxLines;
final ValueChanged<String>? onChanged;
final FormFieldValidator<String>? validator;
final Widget? suffixIcon;
final Widget? prefixIcon;
final BoxConstraints? suffixIconConstraints;
final BoxConstraints? prefixIconConstraints;
final TextInputType? keyboardType;
final List<TextInputFormatter>? inputFormatters;
final Iterable<String>? autofillHints;
final VoidCallback? onEditingComplete;
final AutovalidateMode autovalidateMode;
@override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
enabled: enabled,
obscureText: obscureText,
maxLines: maxLines,
onChanged: onChanged,
validator: validator,
keyboardType: keyboardType,
inputFormatters: inputFormatters,
autofillHints: autofillHints,
onEditingComplete: onEditingComplete,
autovalidateMode: autovalidateMode,
cursorColor: context.inputTheme.focusedTextDefault,
cursorHeight: 16,
// Text style changes with state / نمط النص يتغير مع الحالة
style: WidgetStateTextStyle.resolveWith((states) {
final color = states.contains(WidgetState.disabled)
? context.inputTheme.disabledText
: context.inputTheme.defaultText;
return context.typography.inputPlaceHolder.copyWith(color: color);
}),
decoration: InputDecoration(
labelText: labelText,
helperText: helperText,
errorText: errorText,
filled: true,
fillColor: enabled
? context.inputTheme.defaultColor
: context.inputTheme.disabledColor,
hoverColor: Colors.transparent,
focusColor: Colors.transparent,
// Label style / نمط التسمية
labelStyle: WidgetStateTextStyle.resolveWith((states) {
Color color;
if (states.contains(WidgetState.error)) color = context.inputTheme.errorTextDefault;
else if (states.contains(WidgetState.focused)) color = context.inputTheme.focusedOnBrand;
else if (states.contains(WidgetState.disabled)) color = context.inputTheme.disabledText;
else color = context.inputTheme.defaultText;
return context.typography.inputPlaceHolder.copyWith(color: color);
}),
// Border changes with state / الحدود تتغير مع الحالة
border: WidgetStateOutlineInputBorder.resolveWith((states) {
Color borderColor;
if (states.contains(WidgetState.error)) borderColor = context.inputTheme.borderError;
else if (states.contains(WidgetState.focused)) borderColor = context.inputTheme.borderFocused;
else if (states.contains(WidgetState.disabled)) borderColor = context.inputTheme.borderDisabled;
else if (states.contains(WidgetState.hovered)) borderColor = context.inputTheme.borderHover;
else borderColor = context.inputTheme.borderDefault;
return OutlineInputBorder(
borderRadius: const BorderRadius.all(AppRadius.md),
borderSide: BorderSide(color: borderColor),
);
}),
suffixIcon: suffixIcon,
prefixIcon: prefixIcon,
suffixIconConstraints: suffixIconConstraints,
prefixIconConstraints: prefixIconConstraints,
errorStyle: context.typography.inputHint.copyWith(
color: context.inputTheme.errorTextDefault,
),
),
);
}
}الخطوة 18: هيكلة الـ assets كباكدج منفصل مع مصدر حقيقة واحد
Assets Package Structure:
assets/ # Separate Flutter package / باكدج Flutter منفصل
├── lib/
│ └── app_assets.dart # SSOT - Single Source of Truth
├── fonts/
│ └── inter/ # Export fonts via lib/ folder
├── vectors/ # SVGs - compiled at build time
│ ├── icons/
│ │ ├── search.svg
│ │ └── person.svg
│ └── illustrations/
│ └── empty_state.svg
└── rasters/ # PNGs, JPEGs, WebPs
├── logo_full.png
└── placeholder.webp
AppAssets SSOT class:
/// {@template app_assets}
/// Single Source of Truth for all app assets.
/// المصدر الوحيد للحقيقة لكل أصول التطبيق.
/// {@endtemplate}
abstract class AppAssets {
// SVG Vectors - compiled via flutter_svg / مُجمَّعة بـ flutter_svg
static const searchIcon = AssetBytesLoader(
'vectors/icons/search.svg', packageName: 'assets',
);
static const personIcon = AssetBytesLoader(
'vectors/icons/person.svg', packageName: 'assets',
);
static const emptyStateIllustration = AssetBytesLoader(
'vectors/illustrations/empty_state.svg', packageName: 'assets',
);
// Rasters / صور نقطية
static const logoFull = 'rasters/logo_full.png';
static const placeholder = 'rasters/placeholder.webp';
}
// Usage / الاستخدام:
SvgPicture(loader: AppAssets.searchIcon)
Image.asset(AppAssets.logoFull, package: 'assets')Important
Always pass packageName: 'assets' to AssetBytesLoader and package: 'assets' to Image.asset when loading assets from a separate package - Flutter won't find them without it.
مرّر دائماً packageName: 'assets' لـ AssetBytesLoader و package: 'assets' لـ Image.asset عند تحميل assets من باكدج منفصل - Flutter لن يجدها بدونه.
الخطوة 19: تبني هيكل المشروع الموصى به في الإنتاج
Production-Ready Structure:
my_design_system/ # Standalone Flutter package
│
├── lib/
│ ├── src/
│ │ │
│ │ ├── tokens/ # Layer 0: Raw values / القيم الخام
│ │ │ ├── app_colors.dart
│ │ │ ├── app_spacing.dart
│ │ │ ├── app_radius.dart
│ │ │ └── app_shadow.dart
│ │ │
│ │ ├── foundations/ # Layer 1: Themes + Typography
│ │ │ ├── app_typography.dart
│ │ │ ├── app_button_theme.dart
│ │ │ ├── app_input_theme.dart
│ │ │ ├── app_checkbox_theme.dart
│ │ │ ├── app_toggle_theme.dart
│ │ │ ├── app_badge_theme.dart
│ │ │ ├── app_avatar_theme.dart
│ │ │ ├── app_navigation_theme.dart
│ │ │ ├── app_layout_theme.dart
│ │ │ └── app_theme.dart # Aggregator / المجمّع
│ │ │
│ │ ├── atoms/ # Layer 2: Indivisible widgets
│ │ │ ├── buttons/
│ │ │ │ ├── app_text_button.dart # Abstract base
│ │ │ │ ├── primary_text_button.dart
│ │ │ │ ├── secondary_text_button.dart
│ │ │ │ ├── outline_text_button.dart
│ │ │ │ └── ghost_text_button.dart
│ │ │ ├── app_text_field.dart
│ │ │ ├── app_icon.dart
│ │ │ ├── app_avatar.dart
│ │ │ ├── app_badge.dart
│ │ │ ├── app_toggle.dart
│ │ │ ├── app_checkbox.dart
│ │ │ └── app_radio.dart
│ │ │
│ │ ├── molecules/ # Layer 3: Atomic combinations
│ │ │ ├── search_field.dart
│ │ │ ├── user_card.dart
│ │ │ ├── form_row.dart
│ │ │ └── nav_item.dart
│ │ │
│ │ ├── organisms/ # Layer 4: Complex sections
│ │ │ ├── login_form.dart
│ │ │ ├── app_header.dart
│ │ │ ├── bottom_nav_bar.dart
│ │ │ └── product_list.dart
│ │ │
│ │ ├── templates/ # Layer 5: Layout skeletons
│ │ │ ├── auth_template.dart
│ │ │ ├── main_template.dart
│ │ │ └── dashboard_template.dart
│ │ │
│ │ ├── scope/ # Theme provisioning
│ │ │ ├── theme_scope.dart # InheritedWidget
│ │ │ └── theme_scope_widget.dart # StatefulWidget + persistence
│ │ │
│ │ └── extensions/ # BuildContext sugar
│ │ └── theme_ext.dart
│ │
│ └── my_design_system.dart # Public API - exports everything
│
├── test/
│ ├── atoms/
│ ├── molecules/
│ └── organisms/
│
assets/ # Separate assets package
├── lib/app_assets.dart
├── fonts/
├── vectors/
└── rasters/
Public API barrel file:
// my_design_system.dart - export everything consumers need
library my_design_system;
// Tokens
export 'src/tokens/app_colors.dart';
export 'src/tokens/app_spacing.dart';
export 'src/tokens/app_radius.dart';
export 'src/tokens/app_shadow.dart';
// Foundations
export 'src/foundations/app_typography.dart';
export 'src/foundations/app_theme.dart';
export 'src/foundations/app_button_theme.dart';
// ... all foundations
// Atoms
export 'src/atoms/buttons/primary_text_button.dart';
export 'src/atoms/app_text_field.dart';
// ... all atoms
// Molecules + Organisms + Templates
// ...
// Scope
export 'src/scope/theme_scope.dart';
export 'src/scope/theme_scope_widget.dart';
export 'src/extensions/theme_ext.dart';الخطوة 20: اختبار مكونات الـ Design System
Testing Strategy:
// Widget test for an Atom / اختبار widget لذرّة
void main() {
group('PrimaryTextButton', () {
testWidgets('renders label correctly', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(extensions: [AppTheme.light()]),
home: Scaffold(
body: PrimaryTextButton(
label: 'Test Button',
onTap: () {},
),
),
),
);
expect(find.text('Test Button'), findsOneWidget);
});
testWidgets('shows disabled state when onTap is null', (tester) async {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(extensions: [AppTheme.light()]),
home: const Scaffold(
body: PrimaryTextButton(label: 'Disabled'),
),
),
);
final button = tester.widget<ElevatedButton>(find.byType(ElevatedButton));
expect(button.onPressed, isNull);
});
});
}الخطوة 21: استخدام Widgetbook كتالوج للمكونات
Widgetbook Setup:
// widgetbook/main.dart
@App()
class WidgetbookApp extends StatelessWidget {
const WidgetbookApp({super.key});
@override
Widget build(BuildContext context) {
return Widgetbook.material(
addons: [
MaterialThemeAddon(
themes: [
WidgetbookTheme(name: 'Light', data: ThemeData(extensions: [AppTheme.light()])),
WidgetbookTheme(name: 'Dark', data: ThemeData(extensions: [AppTheme.dark()])),
],
),
DeviceFrameAddon(devices: [Devices.ios.iPhone13, Devices.android.samsungGalaxyS20]),
TextScaleAddon(),
],
directories: [
WidgetbookCategory(
name: 'Atoms',
children: [
WidgetbookComponent(
name: 'PrimaryTextButton',
useCases: [
WidgetbookUseCase(
name: 'Default',
builder: (context) => PrimaryTextButton(
label: context.knobs.string(label: 'Label', initialValue: 'Click me'),
onTap: () {},
appButtonSize: context.knobs.list(
label: 'Size',
options: AppButtonSize.values,
),
),
),
WidgetbookUseCase(
name: 'Disabled',
builder: (context) => const PrimaryTextButton(label: 'Disabled'),
),
],
),
],
),
],
);
}
}Tip
Run Widgetbook as a separate Flutter app (not inside your main app) - flutter run -d chrome -t widgetbook/main.dart. This gives you a live component catalog in the browser.
شغّل Widgetbook كتطبيق Flutter منفصل - flutter run -d chrome -t widgetbook/main.dart. هذا يعطيك كتالوجاً حياً للمكونات في المتصفح.
الخطوة 22: تجنب الأخطاء الشائعة للـ Design System في Flutter
❌ Anti-Patterns / الأنماط السلبية:
| Anti-Pattern / الخطأ | Problem / المشكلة | Fix / الحل |
|---|---|---|
Hardcoded Color(0xFF347AF6) in widget |
Can't theme or swap colors | Use context.buttonTheme.primaryDefault |
Named token buttonColor |
Couples token to component | Use brand.500 - semantic naming |
StatefulWidget for every atom |
Unnecessary overhead | Use StatelessWidget when no internal state |
| Huge organism doing 5 things | Untestable, unmaintainable | Split into smaller organisms |
| No barrel file exports | Messy consumer imports | One design_system.dart that exports all |
| Copy-pasting component styles | Drift over time | Abstract base class pattern |
| Assets in main app package | No SSOT, hard to share | Dedicated assets package |
✅ Best Practices / أفضل الممارسات:
✅ Tokens have no component names
✅ Every atom is const-constructible
✅ Themes use ThemeExtension + lerp
✅ BuildContext has extension methods
✅ One barrel file as public API
✅ Assets in a separate dedicated package
✅ Widgetbook for visual regression testing
✅ Abstract base for component families (buttons)
✅ Dark mode factory for every ThemeExtension
✅ Pages are the only layer that knows about state management
الخطوة 23: إصدار والحفاظ على الـ Design System كباكدج مشترك
Versioning Strategy:
- Use semantic versioning:
MAJOR.MINOR.PATCH. - MAJOR - breaking API changes (rename token, remove component).
- MINOR - new components or tokens added (backward compatible).
- PATCH - bug fixes, style tweaks (fully backward compatible).
- Use
melosfor monorepo management when design system and apps share one repo. - Publish to a private pub server (e.g.
pub.devprivate or self-hosted) for cross-team sharing.
استراتيجية الإصدار:
- استخدم semantic versioning:
MAJOR.MINOR.PATCH. - MAJOR - تغييرات كسر متوافقة (إعادة تسمية token، حذف مكوّن).
- MINOR - إضافة مكونات أو tokens جديدة (متوافقة للخلف).
- PATCH - إصلاح أخطاء، تعديلات نمط (متوافقة كلياً).
- استخدم
melosلإدارة الـ monorepo عندما يتشارك الـ design system والتطبيقات مستودعاً واحداً. - انشر على pub server خاص لمشاركة عبر الفرق.
مرجع سريع - ورقة غش الهرمية الكاملة
Tokens → AppColors, AppSpacing, AppRadius, AppShadow
↓
Foundations → AppTypography, AppButtonTheme, AppInputTheme → AppTheme
↓
Atoms → PrimaryButton, AppTextField, AppIcon, AppAvatar
↓
Molecules → SearchField, UserCard, FormRow, NavItem
↓
Organisms → LoginForm, AppHeader, BottomNavBar, ProductList
↓
Templates → AuthTemplate, MainTemplate, DashboardTemplate
↓
Pages → LoginPage, HomeScreen, ProfilePage
Consumers use:
context.buttonTheme.primaryDefault ← Button color
context.typography.titleSmall ← Text style
context.inputTheme.borderFocused ← Input border
AppSpacing.xl ← 16.0
AppRadius.md ← Radius.circular(8)
AppColors.brand.shade500 ← Primary brand color
AppShadow.sm ← List<BoxShadow>
AppAssets.searchIcon ← SVG loader
تم إعداد هذا الدليل كمرجع شامل لبناء Design Systems احترافية في Flutter
Prepared as a complete reference for building professional Design Systems in Flutter
If this guide helped you build a better Flutter Design System, consider giving it a star ⭐ - it takes 2 seconds and helps others find it!
إذا ساعدك هذا الدليل في بناء Design System أفضل في Flutter، امنحه Star ⭐ - لن يأخذ منك سوى ثانيتين وسيساعد الآخرين في إيجاده!
Share it with your Flutter team - a shared reference means fewer architecture debates.
شاركه مع فريق Flutter الخاص بك - مرجع مشترك يعني نقاشات معمارية أقل.