الفصل التاسع: الواجهة والثيمات Chapter 09: UI Theming & Responsiveness
إزاي خلينا MRE CashBook جميل ومريح على كل الأجهزة والظروف.
التصميم في MRE CashBook مش مجرد ألوان، هو "نظام" (Design System). بنعتمد فيه على 3 ركائز أساسية:
التجريد (Abstraction): الألوان مش مكتوبة هارد كود، هي معرفة في كلاسات خاصة.
السياق (Context): بنوصل للألوان من الـ Context بسهولة، وده بيضمن إن التصميم يتغير لحظياً لو قلبنا Dark Mode.
المرونة (Material 3): بنستخدم أحدث معايير جوجل في التصميم لضمان شكل مودرن وانسيابي.
كلاس AppTheme هو القلب النابض للتصميم. بيجمع بين الـ ColorScheme الأساسي والإضافات الخاصة بينا (Extensions).
final class AppTheme {
// Light Mode Build
static ThemeData get lightTheme => _build(Brightness.light, _lightExtension);
// Dark Mode Build
static ThemeData get darkTheme => _build(Brightness.dark, _darkExtension);
static ThemeData _build(Brightness brightness, AppColorsExtension ext) {
final ColorScheme cs = ColorScheme.fromSeed(
seedColor: ext.primary,
brightness: brightness,
);
// ... Returns ThemeData with full Material 3 configuration
}
}
في تطبيقات الحسابات، الألوان ليها معنى. الأخضر يعني "دخل" والأحمر يعني "خارج". عشان كدة ضفنا حقول مخصصة للثيم:
class AppColorsExtension extends ThemeExtension<AppColorsExtension> {
final Color income; // Emerald Green
final Color expense; // Coral Red
final Color surfaceCard;
final LinearGradient masterBalanceGradient;
// ... copyWith and lerp logic
}
بالطريقة دي، لما نغير من Light لـ Dark، "الأخضر" بتاع الـ Income بيتغير لدرجة أهدى تناسب العين في الضلمة، والتغيير بيحصل بسلاسة (Interpolation) بفضل ميزة lerp.
مش منطقي نكتب Theme.of(context).extension<AppColorsExtension>()! في كل مكان. عشان كدة استخدمنا الـ Extensions:
extension BuildContextExtension on BuildContext {
AppColorsExtension get colors => Theme.of(this).extension<AppColorsExtension>()!;
TextTheme get textTheme => Theme.of(this).textTheme;
ColorScheme get colorScheme => Theme.of(this).colorScheme;
}
التطبيق مش بس بيشتغل على الموبايل، هو بيغير "شخصيته" لما تفتحه على تابلت أو آيباد. استخدمنا ResponsiveLayout عشان نحقق ده:
class ResponsiveLayout extends StatelessWidget {
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth > AppSizes.tabletBreakpoint) {
return desktopBody;
} else if (constraints.maxWidth >= AppSizes.mobileBreakpoint) {
return tabletBody;
} else {
return mobileBody;
}
},
);
}
}
لاحظ استخدام AnimatedSwitcher في الكود الحقيقي؛ ده بيخلي الانتقال بين الموبايل والتابلت ناعم (Fade Transition) مش مجرد خبط لزق.
من أجمل الميزات في MRE CashBook هي الـ Modals. لو إنت على موبايل بتطلع من تحت (Bottom Sheet)، ولو على تابلت بتظهر في النص (Centered Dialog):
static Future<T?> showAdaptiveModal<T>(BuildContext context, {required Widget child}) {
if (ResponsiveLayout.isMobile(context)) {
return _showMobileSheet(context, child: child);
}
return _showDesktopDialog(context, child: child);
}
ده بيوفر تجربة مستخدم (UX) ممتازة، لأن الـ Bottom Sheet مريحة للإيد الواحدة في الموبايل، بينما الـ Dialog أحسن في المساحات الكبيرة.
الوضع الليلي في MRE CashBook مش مجرد خلفية سودة. هو تبديل كامل لمجموعة الألوان عشان نحافظ على تباين النصوص (Accessibility).
التخزين: بنحفظ اختيار المستخدم (Light / Dark / System) في الـ SharedPreferences.
البث: الـ AppSettingsCubit بيبث الثيم المختار للتطبيق كله.
التفاعل: الـ MaterialApp بيستقبل الثيم ويحدث الواجهة فوراً.
BlocBuilder<AppSettingsCubit, AppSettingsState>(
builder: (context, state) {
return MaterialApp(
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: state.themeMode,
// ...
);
},
)
أشهر المشاكل اللي ممكن تقابلك وأنت بتعدل في الثيم:
المشكلة: المسافات مش متناسقة بين الشاشات.
الحل: دايماً استخدم AppSizes. لو استخدمت أرقام ثابتة زي 15 أو 20 يدوياً، الشكل هيبوظ في الشاشات المختلفة.
المشكلة: الكلام مش باين لما اقلب Dark Mode.
الحل: اتأكد إنك بتستخدم context.textTheme.bodyMedium بدل ما تدي لون ثابت للـ Text. الـ Theme هو اللي بيعرف يغير لون الخط حسب الخلفية.
primary في كلاس AppColors. الـ ColorScheme.fromSeed هيتكفل بتغيير الدرجات المشتقة منه في كل حتة.primary value in AppColors. ColorScheme.fromSeed will automatically update all derived shades.ResponsiveLayout والـ AdaptiveOverlays، الواجهة بتتحول لـ Grid Layout و Dialogs عشان تستغل المساحة الكبيرة.ResponsiveLayout and AdaptiveOverlays, the UI adapts to Grid Layouts and centered Dialogs for larger screens.- ThemeExtension: وسيلة لإضافة ألوان مخصصة مش موجودة أساساً في الـ ThemeData.
- Lerp: تقنية حسابية بتخلي الألوان تتغير بنعومة أثناء الانتقال.
- Breakpoint: عرض معين للشاشة التطبيق عنده بيغير شكله (مثلاً 600px).
- Adaptive: القدرة على تغيير نوع الـ Widget (زي Bottom Sheet لـ Dialog).
- Responsive: القدرة على تغيير حجم الـ Widget أو ترتيبه (زي Column لـ Row).
عشان نحافظ على شكل ثابت في كل التطبيق، مابنكتبش ويدجتز من الصفر كل شوية. إحنا عندنا "مكتبة داخلية" في lib/core/widgets:
| Widget Name | Purpose (AR) | Design Feature |
|---|---|---|
AppTextField | إدخال البيانات. | حواف دائرية (RadiusMr) وتلوين ذكي عند الخطأ. |
AppListTile | عرض العمليات. | Ripple effect كامل ومسافات متناسقة. |
AppSwitchTile | الإعدادات. | بيستخدم ألوان الـ primary في حالة التفعيل. |
AppCustomScrollView | القوائم الطويلة. | بيدعم الـ Slivers والـ Sticky Headers. |
أي ويدجت جديد لازم يمر على الـ Core الأول؛ دي القاعدة الذهبية عندنا عشان نمنع تكرار الكود (DRY Principle).
الخط هو اللي بيوصل المعلومة. في MRE CashBook اخترنا خطوط واضحة بتدعم العربي والإنجليزي بنفس الكفاءة.
textTheme: TextTheme(
displayLarge: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
titleMedium: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
bodyMedium: TextStyle(fontSize: 14, fontWeight: FontWeight.normal),
),
استخدمنا حزمة Google Fonts عشان نضمن إن الخط يظهر بنفس الشكل على كل إصدارات أندرويد و iOS، وربطنا كل ده بالـ fontFamily في الـ ThemeData.
| Concept | Arabic Value | English Value |
|---|---|---|
| Primary Color | الأزرق البترولي الهادي. | Calm Petrol Blue. |
| Corner Radius | 8px - 16px (Medium). | Standardized Radii. |
| Transitions | Fade & Scale. | Smooth Switcher. |
| Breakpoints | 600px / 900px. | Responsive Steps. |
عشان التطبيق يفضل سريع (60 FPS) حتى على الأجهزة الضعيفة، اتبعنا القواعد دي:
استخدام const: أي ويدجت مش بيتغير لازم يكون const عشان Flutter مايعيدش بناؤه (Rebuild) بدون داعي.
تجنب Magic Numbers: مفيش Padding أو Radius مكتوب يدوياً. دايماً AppSizes.p16 أو AppSizes.radiusMd.
الأنيميشن الهادي: استخدمنا AppAnimations.smooth لكل الانتقالات عشان التجربة تكون مريحة للعين.
AdaptiveOverlays اللي بيفحص عرض الشاشة ويختار النوع المناسب أوتوماتيك.AdaptiveOverlays class which checks the screen width and selects the appropriate type automatically.في الفصل ده اتعلمنا إزاي نبني نظام تصميم متكامل، بيدعم الوضع الليلي، وبيستوعب كل أحجام الشاشات باحترافية.
| Feature | Implementation Tool | Primary Goal |
|---|---|---|
| Centralized Theme | AppTheme |
Single source of truth for styles. |
| Semantic Colors | AppColorsExtension |
Meaningful colors (Income/Expense). |
| Clean Access | context.colors |
Developer experience and brevity. |
| Dark Mode | System / User Preference | User comfort and accessibility. |
| Layout Adaptivity | ResponsiveLayout |
Support Mobile / Tablet / Desktop. |
| Overlay Adaptivity | AdaptiveOverlays |
Optimal UX based on form factor. |