بناء واجهة المستخدم الأساسية بمكونات قابلة لإعادة الاستخدام
بناء واجهة المستخدم الأساسية بمكونات قابلة لإعادة الاستخدام
تطبيق Flutter الاحترافي ليس مجموعة من الشاشات المنفصلة — بل هو نظام متكامل. من خلال تعريف AppTheme مركزي، واستخراج أنماط واجهة المستخدم إلى ودجات قابلة لإعادة الاستخدام، وتطبيق تقنيات التخطيط المتجاوب بشكل منتظم، تحصل على واجهة مستخدم متماسكة وسهلة الصيانة والتوسع. يتناول هذا الدرس كل طبقة من تلك الطبقات في مشروع الكابستون.
مركزة نظام التصميم باستخدام AppTheme
جميع القرارات البصرية — الألوان والطباعة والأشكال والمسافات — تنتمي إلى مكان واحد. أنشئ ملفاً مخصصاً lib/core/theme/app_theme.dart يكشف عن فئة AppTheme واحدة مع مصانع ThemeData ثابتة للوضعين الفاتح والداكن.
lib/core/theme/app_theme.dart
import 'package:flutter/material.dart';
class AppTheme {
AppTheme._(); // منع الاستنساخ
static const Color _primaryColor = Color(0xFF2563EB);
static const Color _secondaryColor = Color(0xFF10B981);
static const Color _errorColor = Color(0xFFEF4444);
static ThemeData get light => ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: _primaryColor,
secondary: _secondaryColor,
error: _errorColor,
brightness: Brightness.light,
),
textTheme: _buildTextTheme(Brightness.light),
elevatedButtonTheme: _buildButtonTheme(),
cardTheme: const CardTheme(
elevation: 2,
margin: EdgeInsets.all(8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
),
appBarTheme: const AppBarTheme(
centerTitle: true,
elevation: 0,
),
);
static ThemeData get dark => light.copyWith(
colorScheme: ColorScheme.fromSeed(
seedColor: _primaryColor,
secondary: _secondaryColor,
error: _errorColor,
brightness: Brightness.dark,
),
textTheme: _buildTextTheme(Brightness.dark),
);
static TextTheme _buildTextTheme(Brightness brightness) {
final baseColor =
brightness == Brightness.light ? Colors.black87 : Colors.white;
return TextTheme(
displayLarge: TextStyle(fontSize: 32, fontWeight: FontWeight.bold, color: baseColor),
titleLarge: TextStyle(fontSize: 20, fontWeight: FontWeight.w600, color: baseColor),
bodyMedium: TextStyle(fontSize: 14, color: baseColor),
labelLarge: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
);
}
static ElevatedButtonThemeData _buildButtonTheme() =>
ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
);
}
اربط AppTheme بـ MaterialApp مرة واحدة في main.dart:
ربط AppTheme في MaterialApp
MaterialApp.router(
title: 'Capstone App',
theme: AppTheme.light,
darkTheme: AppTheme.dark,
themeMode: ThemeMode.system,
routerConfig: appRouter,
);
Theme.of(context).colorScheme أو Theme.of(context).textTheme حتى يستجيب التطبيق بأكمله فوراً عند تغيير السمة.بناء مكونات ودجات قابلة لإعادة الاستخدام
حدد الأنماط البصرية المتكررة عبر شاشاتك واستخرج كل منها في StatelessWidget مخصص. يجب أن يقبل كل ودجت قابل لإعادة الاستخدام فقط الحد الأدنى من البيانات التي يحتاجها عبر معاملات المنشئ، ويفوّض جميع الاستدعاءات إلى الوالد. هذا يجعل المكونات نقية وقابلة للتنبؤ وسهلة الاختبار.
المرشحون الشائعون في مشروع كابستون نموذجي يشملون:
- AppButton — زر الإجراء الأساسي مع حالة التحميل
- AppTextField — حقل نص محقق مع زخرفة متسقة
- SectionHeader — عنوان عريض مع إجراء اختياري في النهاية
- ItemCard — قالب بطاقة لعناصر القائمة أو الشبكة
- EmptyState — رسم توضيحي ورسالة عندما تكون القائمة فارغة
- LoadingOverlay —
CircularProgressIndicatorفي المنتصف
lib/shared/widgets/app_button.dart
import 'package:flutter/material.dart';
class AppButton extends StatelessWidget {
final String label;
final VoidCallback? onPressed;
final bool isLoading;
final Color? backgroundColor;
const AppButton({
super.key,
required this.label,
this.onPressed,
this.isLoading = false,
this.backgroundColor,
});
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return ElevatedButton(
onPressed: isLoading ? null : onPressed,
style: ElevatedButton.styleFrom(
backgroundColor: backgroundColor ?? cs.primary,
foregroundColor: cs.onPrimary,
),
child: isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: Text(label),
);
}
}
isLoading كمعامل في المنشئ — بدلاً من حالة داخلية — يعني أن الوالد يتحكم بالكامل في دورة حياة الزر. هذا النمط يعمل بسلاسة مع Provider وRiverpod وBloc لأن الوالد يعيد بناء الزر كلما تغيرت الحالة غير المتزامنة.التخطيط المتجاوب مع LayoutBuilder وMediaQuery
يجب أن تتكيف الشاشات مع قيود الهاتف والجهاز اللوحي وسطح المكتب. تغطي أداتان معظم السيناريوهات:
MediaQuery.of(context).size— استخدمها في أعلى الشاشة للتفرع بين التخطيطات المضغوطة والموسعة.LayoutBuilder— استخدمها داخل شجرة فرعية للودجت للاستجابة لحجم الصندوق المتاح، وليس حجم الشاشة.
شبكة متجاوبة باستخدام LayoutBuilder
class ResponsiveItemGrid extends StatelessWidget {
final List<Item> items;
const ResponsiveItemGrid({super.key, required this.items});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final columns = constraints.maxWidth < 600 ? 2 : 4;
return GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columns,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 3 / 4,
),
itemCount: items.length,
itemBuilder: (context, index) => ItemCard(item: items[index]),
);
},
);
}
}
تجميع الشاشات الرئيسية
مع جاهزية نظام التصميم والودجات القابلة لإعادة الاستخدام، يصبح بناء كل شاشة رئيسية تمريناً في التركيب. يستورد كل ملف شاشة المكونات المشتركة، ويقرأ بياناته من طبقة الحالة (Provider، Riverpod، إلخ)، ويرتب الودجات باستخدام رموز المسافات والألوان في السمة. تبقى الشاشات نفسها نحيفة — لا منطق أعمال، ولا ألوان خام، ولا كود تخطيط مكرر.
ملخص
في هذا الدرس أرسيت أسس واجهة مستخدم Flutter قابلة للتطوير:
- AppTheme مركزي يوفر ألواناً وطباعة وأنماط مكونات متسقة في جميع أنحاء التطبيق.
- مكتبة من الودجات عديمة الحالة القابلة لإعادة الاستخدام (AppButton، AppTextField، ItemCard، EmptyState) تقبل فقط البيانات التي تحتاجها وتفوّض الأحداث للأعلى.
- تخطيطات متجاوبة مبنية بـ LayoutBuilder وMediaQuery تتكيف بسلاسة مع أحجام شاشات مختلفة.
- ملفات الشاشات تبقى نحيفة بتركيب مكونات قابلة لإعادة الاستخدام بدلاً من تكرار كود واجهة المستخدم.