أنماط إعداد الودجات والتصميم المرئي
أنماط إعداد الودجات والتصميم المرئي
لا تكون الودجات المخصصة مفيدةً حقاً إلا حين تستطيع التكيّف مع التصميم البصري للتطبيق. تشفير الألوان والأحجام مباشرةً داخل الودجت يجعله هشاً: في كل مرة تتغير فيها لوحة الألوان تضطر للبحث في كل مكان يُستخدم فيه. يوفر Flutter آليتين قويتين متكاملتين—امتدادات ThemeData وInheritedWidget—تتيحان لك إيصال قرارات التنسيق من مصدر واحد للحقيقة إلى كل ودجت في الشجرة، دون تمرير المعاملات يدوياً عبر طبقات الودجات الوسيطة (prop-drilling).
لماذا يُعدّ تمرير الخصائص عبر الطبقات مشكلة؟
تخيّل شجرة ودجات عميقة: App → Screen → Section → Card → Badge. إذا احتاج Badge إلى لون تمييزي مُعرَّف على مستوى التطبيق، يقتضي النهج الساذج تمرير ذلك اللون عبر كل مُنشئ وسيط. ينتج عن ذلك اقتران وثيق، وإعادة هيكلة مؤلمة، وإثقال API كل ودجت بمعاملات لا تعنيه. الهدف هو نقل الإعداد خارج قوائم المعاملات إلى سياق مشترك محدد النطاق بالشجرة.
Theme وProvider. تعلّمه مباشرةً يمنحك فهماً عميقاً لكيفية عمل بحث السياق في Flutter فعلياً.امتدادات ThemeData
تحتوي ThemeData في Flutter على خريطة extensions عامة. يمكنك تسجيل أي كائن كامتداد للسمة واسترجاعه في أي مكان عبر Theme.of(context). هذه هي الطريقة الاصطلاحية في Flutter لإرفاق رموز التصميم المخصصة بالسمة القياسية.
لإنشاء امتداد سمة:
- وسّع
ThemeExtension<T>ونفّذcopyWithوlerp. - سجّل نسخة في
MaterialApp.theme.extensions. - اقرأها في أي مكان باستخدام
Theme.of(context).extension<T>().
تعريف وتسجيل امتداد ThemeExtension
// 1. تعريف الامتداد
@immutable
class AppBadgeTheme extends ThemeExtension<AppBadgeTheme> {
const AppBadgeTheme({
required this.backgroundColor,
required this.textColor,
required this.borderRadius,
});
final Color backgroundColor;
final Color textColor;
final double borderRadius;
@override
AppBadgeTheme copyWith({
Color? backgroundColor,
Color? textColor,
double? borderRadius,
}) {
return AppBadgeTheme(
backgroundColor: backgroundColor ?? this.backgroundColor,
textColor: textColor ?? this.textColor,
borderRadius: borderRadius ?? this.borderRadius,
);
}
@override
AppBadgeTheme lerp(AppBadgeTheme? other, double t) {
if (other is! AppBadgeTheme) return this;
return AppBadgeTheme(
backgroundColor: Color.lerp(backgroundColor, other.backgroundColor, t)!,
textColor: Color.lerp(textColor, other.textColor, t)!,
borderRadius: lerpDouble(borderRadius, other.borderRadius, t)!,
);
}
}
// 2. التسجيل في MaterialApp
MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
extensions: <ThemeExtension<dynamic>>[
const AppBadgeTheme(
backgroundColor: Color(0xFF3949AB),
textColor: Colors.white,
borderRadius: 8.0,
),
],
),
home: const HomeScreen(),
)
// 3. الاستهلاك في أي ودجت — دون تمرير خصائص عبر الطبقات
class StatusBadge extends StatelessWidget {
const StatusBadge({super.key, required this.label});
final String label;
@override
Widget build(BuildContext context) {
final badge = Theme.of(context).extension<AppBadgeTheme>()!;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: badge.backgroundColor,
borderRadius: BorderRadius.circular(badge.borderRadius),
),
child: Text(label, style: TextStyle(color: badge.textColor)),
);
}
}
lerp بشكل صحيح حتى لو اعتقدت أنك لن تُحرّك انتقالات السمة أبداً. تستدعي انتقالات السمة في Flutter بين الوضع الفاتح والداكن lerp على كل امتداد مسجّل، وتنفيذ معطوب يُنتج تشوهات بصرية خلال ذلك الانتقال.InheritedWidget للإعداد المخصص
عندما تحتاج عائلة ودجات مخصصة إلى إعداد ليس جزءاً من ThemeData أصلاً—كأسلوب محاور مكتبة رسم بياني أو ألوان ملاحظات التحقق في محرك نماذج مخصص—يمكنك نشر ذلك الإعداد عبر InheritedWidget. يستطيع أي سليل استدعاء context.dependOnInheritedWidgetOfExactType<T>() لقراءته وإعادة البناء تلقائياً عند تغيّره.
النمط ثلاثي الخطوات هو: تعريف InheritedWidget، لفّه حول الشجرة الفرعية التي تحتاج البيانات، وكشف مصنع ثابت .of(context) للوصول المريح.
InheritedWidget مخصص لإعداد الودجات
// الخطوة 1 — تعريف فئة البيانات
@immutable
class ChartConfig {
const ChartConfig({
required this.gridColor,
required this.barColor,
required this.labelStyle,
this.showGrid = true,
});
final Color gridColor;
final Color barColor;
final TextStyle labelStyle;
final bool showGrid;
}
// الخطوة 2 — تعريف InheritedWidget
class ChartConfigProvider extends InheritedWidget {
const ChartConfigProvider({
super.key,
required this.config,
required super.child,
});
final ChartConfig config;
// الخطوة 3 — كشف وصول ثابت
static ChartConfig of(BuildContext context) {
final provider = context
.dependOnInheritedWidgetOfExactType<ChartConfigProvider>();
assert(provider != null, 'No ChartConfigProvider found in context');
return provider!.config;
}
@override
bool updateShouldNotify(ChartConfigProvider oldWidget) =>
config != oldWidget.config;
}
// الخطوة 4 — لف الشجرة الفرعية
class DashboardScreen extends StatelessWidget {
const DashboardScreen({super.key});
@override
Widget build(BuildContext context) {
return ChartConfigProvider(
config: ChartConfig(
gridColor: Colors.grey.shade300,
barColor: Theme.of(context).colorScheme.primary,
labelStyle: Theme.of(context).textTheme.labelSmall!,
),
child: const Column(
children: [RevenueChart(), UsersChart()],
),
);
}
}
// الخطوة 5 — الاستهلاك في أي مكان أسفله دون تمرير عبر المُنشئات
class RevenueChart extends StatelessWidget {
const RevenueChart({super.key});
@override
Widget build(BuildContext context) {
final cfg = ChartConfigProvider.of(context);
return CustomPaint(
painter: BarChartPainter(
barColor: cfg.barColor,
gridColor: cfg.gridColor,
showGrid: cfg.showGrid,
),
);
}
}
الاختيار بين ThemeExtension وInheritedWidget
كلا الآليتين يحلّان مشكلة تمرير الخصائص عبر الطبقات، لكنهما يخدمان نطاقات مختلفة:
- ThemeExtension — الأنسب لرموز التصميم البصري (الألوان والأنصاف وخطوط الطباعة) التي يجب أن تستجيب لتبديل الوضع الفاتح/الداكن وتشارك في انتقالات السمة. تعيش البيانات في
ThemeDataوتُورَث تلقائياً عبر نفس قناة سائر قيم السمة. - InheritedWidget — الأنسب للإعداد السلوكي أو البنيوي غير المرتبط بالسمة البصرية، أو عندما تحتاج إلى تحديد نطاق الإعداد لشجرة فرعية محددة.
updateShouldNotify للقيمة true وبين تغيّر فعلي في البيانات. إذا أعدت true دون شرط، سيُجبر كل إعادة بناء للأب على إعادة بناء جميع المُعتمِدين—حتى حين لم يتغير الإعداد. قارن دائماً القيم القديمة والجديدة.الجمع بين النمطين
تجمع الودجات الواقعية في الغالب بين النمطين: تقرأ رموز الألوان من ThemeExtension للتناسق البصري، وتقرأ الخيارات السلوكية من InheritedWidget مُحدَّد النطاق للمرونة. يُبقي هذا النهج الطبقي رموز التصميم في مكان واحد والإعداد الخاص بالمجال في مكان آخر، مما يجعل مكتبة الودجات الخاصة بك سهلة التنسيق عالمياً وسهلة الإعداد محلياً.
ThemeExtension لإرفاق رموز التصميم المخصصة بـ ThemeData لتطبيقك حتى يتمكن أي ودجت من قراءتها عبر Theme.of(context). استخدم InheritedWidget لتحديد نطاق الإعداد غير المتعلق بالسمة لشجرة فرعية. يُلغي كلا النمطين تمرير الخصائص عبر الطبقات، ويضمنان انتشار أي تغيير واحد تلقائياً، ويُبقيان مُنشئات الودجات الفردية نظيفةً ومركّزةً على مسؤولياتها الخاصة.