التنسيق والتصميم
مقدمة في تنسيق Flutter
يوفر نظام التنسيق في Flutter طريقة قوية ومركزية لتحديد المظهر المرئي لتطبيقك بالكامل. بدلاً من تنسيق كل ودجت بشكل فردي، تعرّف كائن ThemeData يحدد الألوان والطباعة والأشكال وأنماط المكونات على مستوى التطبيق. يمكن لكل ودجت في شجرتك الوصول إلى هذه القيم عبر Theme.of(context)، مما يضمن الاتساق عبر نظام التصميم بأكمله.
مع Material 3 (تصميم المواد 3)، قدم Flutter نهج تنسيق حديث مبني حول ColorScheme والألوان الديناميكية. يغطي هذا الدرس كل ما تحتاجه لبناء نظام تنسيق احترافي وقابل للصيانة.
useMaterial3: false.ThemeData: الأساس
ThemeData هو كائن التكوين المركزي الذي يحدد الخصائص المرئية لتطبيقك. يُمرر إلى معامل theme في MaterialApp.
إعداد ThemeData الأساسي
import 'package:flutter/material.dart';
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'تطبيق منسّق',
theme: ThemeData(
// Material 3 افتراضي في Flutter 3.16+
useMaterial3: true,
// ColorScheme يحدد كل الألوان المستخدمة بواسطة مكونات Material
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF1E88E5), // لون علامتك التجارية
brightness: Brightness.light,
),
// الطباعة
textTheme: const TextTheme(
headlineLarge: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
),
bodyLarge: TextStyle(fontSize: 16),
labelLarge: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
// تخصيص على مستوى المكونات
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
home: const HomeScreen(),
);
}
}
ColorScheme: نظام ألوان Material 3
في Material 3، ColorScheme هو الطريقة الأساسية لتحديد الألوان. ينشئ مجموعة متناسقة من الألوان من لون بذرة واحد، مما يضمن التماسك المرئي عبر جميع المكونات.
ColorScheme.fromSeed
// إنشاء مخطط ألوان كامل من لون بذرة واحد
final lightScheme = ColorScheme.fromSeed(
seedColor: const Color(0xFF6750A4), // بذرة أرجوانية
brightness: Brightness.light,
);
final darkScheme = ColorScheme.fromSeed(
seedColor: const Color(0xFF6750A4),
brightness: Brightness.dark,
);
// المخطط المُنشأ يتضمن:
// primary, onPrimary, primaryContainer, onPrimaryContainer
// secondary, onSecondary, secondaryContainer, onSecondaryContainer
// tertiary, onTertiary, tertiaryContainer, onTertiaryContainer
// error, onError, errorContainer, onErrorContainer
// surface, onSurface, surfaceVariant, onSurfaceVariant
// outline, outlineVariant, shadow, scrim
// inverseSurface, onInverseSurface, inversePrimary
ColorScheme مخصص بألوان العلامة التجارية
// للتحكم الدقيق بالعلامة التجارية، خصص ألواناً محددة
final brandScheme = ColorScheme.fromSeed(
seedColor: const Color(0xFF1565C0),
).copyWith(
// تجاوز ألوان محددة مع الحفاظ على التناسق
primary: const Color(0xFF1565C0),
secondary: const Color(0xFFFF6F00),
tertiary: const Color(0xFF2E7D32),
error: const Color(0xFFD32F2F),
);
// أو بناء مخطط مخصص بالكامل
final customScheme = const ColorScheme(
brightness: Brightness.light,
primary: Color(0xFF1565C0),
onPrimary: Color(0xFFFFFFFF),
primaryContainer: Color(0xFFD1E4FF),
onPrimaryContainer: Color(0xFF001D36),
secondary: Color(0xFFFF6F00),
onSecondary: Color(0xFFFFFFFF),
secondaryContainer: Color(0xFFFFE0B2),
onSecondaryContainer: Color(0xFF2B1700),
surface: Color(0xFFFFFBFF),
onSurface: Color(0xFF1C1B1F),
error: Color(0xFFD32F2F),
onError: Color(0xFFFFFFFF),
errorContainer: Color(0xFFFFDAD6),
onErrorContainer: Color(0xFF410002),
outline: Color(0xFF79747E),
outlineVariant: Color(0xFFCAC4D0),
shadow: Color(0xFF000000),
scrim: Color(0xFF000000),
);
ColorScheme.fromSeed() يستخدم فضاء ألوان HCT (الصبغة، التشبع، الدرجة) لإنشاء ألوان موحدة إدراكياً. لون البذرة يحدد الصبغة والخوارزمية تنشئ لوحات أساسية وثانوية وثلاثية متناسقة مع نسب تباين مناسبة لإمكانية الوصول.TextTheme: نظام الطباعة
يحدد Material 3 نظام طباعة منظم مع أنماط نص محددة لأغراض مختلفة. فئة TextTheme تنظمها في أدوار دلالية.
تكوين TextTheme الكامل
import 'package:google_fonts/google_fonts.dart';
final appTextTheme = TextTheme(
// أنماط العرض -- نص كبير وبارز
displayLarge: GoogleFonts.poppins(
fontSize: 57,
fontWeight: FontWeight.w400,
letterSpacing: -0.25,
),
displayMedium: GoogleFonts.poppins(
fontSize: 45,
fontWeight: FontWeight.w400,
),
displaySmall: GoogleFonts.poppins(
fontSize: 36,
fontWeight: FontWeight.w400,
),
// أنماط العناوين -- رؤوس الأقسام
headlineLarge: GoogleFonts.poppins(
fontSize: 32,
fontWeight: FontWeight.w600,
),
headlineMedium: GoogleFonts.poppins(
fontSize: 28,
fontWeight: FontWeight.w600,
),
headlineSmall: GoogleFonts.poppins(
fontSize: 24,
fontWeight: FontWeight.w600,
),
// أنماط العنوان -- عناوين البطاقات، الحوارات
titleLarge: GoogleFonts.poppins(
fontSize: 22,
fontWeight: FontWeight.w500,
),
titleMedium: GoogleFonts.poppins(
fontSize: 16,
fontWeight: FontWeight.w500,
letterSpacing: 0.15,
),
titleSmall: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w500,
letterSpacing: 0.1,
),
// أنماط الجسم -- نص الفقرات
bodyLarge: GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w400,
letterSpacing: 0.5,
),
bodyMedium: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w400,
letterSpacing: 0.25,
),
bodySmall: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w400,
letterSpacing: 0.4,
),
// أنماط التسمية -- الأزرار، التبويبات، الشرائح
labelLarge: GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w500,
letterSpacing: 0.1,
),
labelMedium: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
),
labelSmall: GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.w500,
letterSpacing: 0.5,
),
);
الوصول إلى بيانات التنسيق
بمجرد تكوين التنسيق، يمكنك الوصول إليه في أي مكان من شجرة الودجات باستخدام Theme.of(context).
استخدام Theme.of(context)
class ProfileCard extends StatelessWidget {
final String name;
final String email;
const ProfileCard({required this.name, required this.email, super.key});
@override
Widget build(BuildContext context) {
// الوصول إلى التنسيق الحالي
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final textTheme = theme.textTheme;
return Card(
// Card يستخدم تلقائياً لون السطح والارتفاع من التنسيق
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: textTheme.headlineSmall?.copyWith(
color: colorScheme.onSurface,
),
),
const SizedBox(height: 4),
Text(
email,
style: textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 12),
FilledButton(
onPressed: () {},
child: const Text('عرض الملف الشخصي'),
),
],
),
),
);
}
}
تنسيق على مستوى المكونات
يتيح لك Material 3 تخصيص المظهر الافتراضي لكل مكون عبر بيانات التنسيق. كل مكون له فئة تنسيق خاصة به.
تنسيق شامل للمكونات
ThemeData buildAppTheme() {
final colorScheme = ColorScheme.fromSeed(
seedColor: const Color(0xFF1E88E5),
);
return ThemeData(
useMaterial3: true,
colorScheme: colorScheme,
// تنسيق شريط التطبيق
appBarTheme: AppBarTheme(
centerTitle: true,
elevation: 0,
scrolledUnderElevation: 2,
backgroundColor: colorScheme.surface,
foregroundColor: colorScheme.onSurface,
titleTextStyle: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
// تنسيق البطاقة
cardTheme: CardTheme(
elevation: 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
clipBehavior: Clip.antiAlias,
),
// تنسيق حقول الإدخال
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: colorScheme.surfaceContainerHighest.withOpacity(0.5),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colorScheme.primary, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: colorScheme.error),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
),
// تنسيق زر الإجراء العائم
floatingActionButtonTheme: FloatingActionButtonThemeData(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
),
// تنسيق شريط التنقل السفلي
bottomNavigationBarTheme: BottomNavigationBarThemeData(
type: BottomNavigationBarType.fixed,
selectedItemColor: colorScheme.primary,
unselectedItemColor: colorScheme.onSurfaceVariant,
showUnselectedLabels: true,
),
// تنسيق الشريحة
chipTheme: ChipThemeData(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
// تنسيق الحوار
dialogTheme: DialogTheme(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(28),
),
),
);
}
امتدادات التنسيق المخصصة
عندما لا تكفي خصائص التنسيق المدمجة في Material، يمكنك إنشاء امتدادات تنسيق مخصصة لإضافة ألوان دلالية أو مسافات أو أي رموز تصميم أخرى خاصة بك.
إنشاء امتدادات تنسيق مخصصة
// تعريف امتداد التنسيق المخصص
class AppColors extends ThemeExtension<AppColors> {
final Color? success;
final Color? onSuccess;
final Color? warning;
final Color? onWarning;
final Color? info;
final Color? onInfo;
final Color? cardGradientStart;
final Color? cardGradientEnd;
const AppColors({
this.success,
this.onSuccess,
this.warning,
this.onWarning,
this.info,
this.onInfo,
this.cardGradientStart,
this.cardGradientEnd,
});
@override
AppColors copyWith({
Color? success,
Color? onSuccess,
Color? warning,
Color? onWarning,
Color? info,
Color? onInfo,
Color? cardGradientStart,
Color? cardGradientEnd,
}) {
return AppColors(
success: success ?? this.success,
onSuccess: onSuccess ?? this.onSuccess,
warning: warning ?? this.warning,
onWarning: onWarning ?? this.onWarning,
info: info ?? this.info,
onInfo: onInfo ?? this.onInfo,
cardGradientStart: cardGradientStart ?? this.cardGradientStart,
cardGradientEnd: cardGradientEnd ?? this.cardGradientEnd,
);
}
@override
AppColors lerp(AppColors? other, double t) {
if (other is! AppColors) return this;
return AppColors(
success: Color.lerp(success, other.success, t),
onSuccess: Color.lerp(onSuccess, other.onSuccess, t),
warning: Color.lerp(warning, other.warning, t),
onWarning: Color.lerp(onWarning, other.onWarning, t),
info: Color.lerp(info, other.info, t),
onInfo: Color.lerp(onInfo, other.onInfo, t),
cardGradientStart: Color.lerp(cardGradientStart, other.cardGradientStart, t),
cardGradientEnd: Color.lerp(cardGradientEnd, other.cardGradientEnd, t),
);
}
}
// تسجيل الامتداد في ThemeData
final theme = ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
extensions: [
const AppColors(
success: Color(0xFF4CAF50),
onSuccess: Color(0xFFFFFFFF),
warning: Color(0xFFFFC107),
onWarning: Color(0xFF000000),
info: Color(0xFF2196F3),
onInfo: Color(0xFFFFFFFF),
cardGradientStart: Color(0xFF1E88E5),
cardGradientEnd: Color(0xFF7C4DFF),
),
],
);
// الوصول إلى الامتداد في أي مكان
class StatusBanner extends StatelessWidget {
final String message;
final StatusType type;
const StatusBanner({required this.message, required this.type, super.key});
@override
Widget build(BuildContext context) {
final appColors = Theme.of(context).extension<AppColors>()!;
final (bgColor, fgColor) = switch (type) {
StatusType.success => (appColors.success!, appColors.onSuccess!),
StatusType.warning => (appColors.warning!, appColors.onWarning!),
StatusType.info => (appColors.info!, appColors.onInfo!),
};
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(8),
),
child: Text(message, style: TextStyle(color: fgColor)),
);
}
}
enum StatusType { success, warning, info }
lerp مطلوبة للانتقالات السلسة بين التنسيقات (مثل الانتقال بين التنسيق الفاتح والداكن). تقوم بالاستيفاء بين نسختين من الامتداد عند عامل t معين (0.0 إلى 1.0).تنسيق محدد النطاق مع ودجت Theme
يمكنك تجاوز التنسيق لشجرة فرعية من الودجات باستخدام ودجت Theme. هذا مفيد لأقسام تطبيقك التي تحتاج معالجة مرئية مختلفة.
تجاوز تنسيق محدد النطاق
class PremiumSection extends StatelessWidget {
const PremiumSection({super.key});
@override
Widget build(BuildContext context) {
return Theme(
// تجاوز التنسيق لهذه الشجرة الفرعية فقط
data: Theme.of(context).copyWith(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFFFFD700), // تنسيق ذهبي
brightness: Theme.of(context).brightness,
),
cardTheme: CardTheme(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(color: Color(0xFFFFD700), width: 2),
),
),
),
child: Column(
children: [
// هذه الودجات تستخدم التنسيق الذهبي
Card(
child: ListTile(
leading: const Icon(Icons.star),
title: const Text('ميزة مميزة'),
subtitle: const Text('وصول حصري'),
),
),
FilledButton(
onPressed: () {},
child: const Text('ترقية الآن'),
),
],
),
);
}
}
مثال عملي: تنسيق علامة تجارية كامل
لنبنِ تنسيقاً كاملاً وجاهزاً للإنتاج يوضح جميع المفاهيم معاً:
تنسيق علامة تجارية للإنتاج
// lib/theme/app_theme.dart
import 'package:flutter/material.dart';
class AppTheme {
// ألوان العلامة التجارية
static const _brandBlue = Color(0xFF1565C0);
static const _brandOrange = Color(0xFFFF6F00);
// ثوابت المسافات (ليست جزءاً من ThemeData لكنها مفيدة)
static const double spacingXs = 4;
static const double spacingSm = 8;
static const double spacingMd = 16;
static const double spacingLg = 24;
static const double spacingXl = 32;
// ثوابت نصف القطر
static const double radiusSm = 8;
static const double radiusMd = 12;
static const double radiusLg = 16;
static const double radiusXl = 28;
static ThemeData get lightTheme {
final colorScheme = ColorScheme.fromSeed(
seedColor: _brandBlue,
brightness: Brightness.light,
);
return _buildTheme(colorScheme);
}
static ThemeData get darkTheme {
final colorScheme = ColorScheme.fromSeed(
seedColor: _brandBlue,
brightness: Brightness.dark,
);
return _buildTheme(colorScheme);
}
static ThemeData _buildTheme(ColorScheme colorScheme) {
return ThemeData(
useMaterial3: true,
colorScheme: colorScheme,
// السقالة
scaffoldBackgroundColor: colorScheme.surface,
// شريط التطبيق
appBarTheme: AppBarTheme(
centerTitle: false,
elevation: 0,
backgroundColor: colorScheme.surface,
foregroundColor: colorScheme.onSurface,
),
// البطاقات
cardTheme: CardTheme(
elevation: 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radiusMd),
),
),
// الأزرار
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radiusSm),
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radiusSm),
),
),
),
// حقول الإدخال
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: colorScheme.surfaceContainerHighest.withOpacity(0.3),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(radiusSm),
borderSide: BorderSide(color: colorScheme.outline),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(radiusSm),
borderSide: BorderSide(color: colorScheme.primary, width: 2),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: spacingMd,
vertical: 14,
),
),
// الفاصل
dividerTheme: DividerThemeData(
color: colorScheme.outlineVariant,
thickness: 1,
space: 1,
),
// الامتدادات
extensions: [
AppColors(
success: const Color(0xFF4CAF50),
onSuccess: Colors.white,
warning: const Color(0xFFFFC107),
onWarning: Colors.black,
info: colorScheme.primary,
onInfo: colorScheme.onPrimary,
cardGradientStart: colorScheme.primaryContainer,
cardGradientEnd: colorScheme.tertiaryContainer,
),
],
);
}
}
// الاستخدام في MaterialApp
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: AppTheme.lightTheme,
darkTheme: AppTheme.darkTheme,
themeMode: ThemeMode.system,
home: const HomeScreen(),
);
}
}
الملخص
- ThemeData هو التكوين المركزي للتصميم المرئي لتطبيقك، يُمرر إلى MaterialApp.
- ColorScheme.fromSeed() ينشئ لوحة ألوان كاملة ومتناسقة من لون بذرة واحد.
- TextTheme يوفر أنماط نص دلالية (عرض، عنوان رئيسي، عنوان، جسم، تسمية) بثلاثة أحجام لكل منها.
- Theme.of(context) يمنحك الوصول إلى التنسيق الحالي في أي مكان من شجرة الودجات.
- تنسيقات المكونات (AppBarTheme, CardTheme, إلخ) تخصص التنسيق الافتراضي لودجات محددة.
- ThemeExtension يتيح لك إضافة رموز تصميم مخصصة (ألوان نجاح، مسافات، إلخ) تتكامل مع نظام التنسيق.
- ودجت Theme تتيح تجاوزات محددة النطاق للأشجار الفرعية التي تحتاج تنسيقاً مختلفاً.
- نظّم كود التنسيق في فئة مخصصة (AppTheme) مع طرق ثابتة لسهولة الصيانة.