SwitchListTile

ما هو SwitchListTile؟

SwitchListTile هو نسخة مريحة من ListTile تحتوي على مفتاح تبديل (Switch) مدمج. تُستخدم عادة لعرض خيار قابل للتشغيل/الإيقاف داخل قائمة (مثل صفحة الإعدادات). يسمح لك بعرض title, subtitle, وsecondary جنبًا إلى جنب مع الـ Switch، والصف كله قابل للنقر لتغيير الحالة (إذا وُفِّر onChanged).

البنية الأساسية (constructor)
SwitchListTile({
  Key? key,
  required bool value,
  required ValueChanged<bool>? onChanged,
  Widget? title,
  Widget? subtitle,
  Widget? secondary,
  bool isThreeLine = false,
  bool dense,
  bool selected = false,
  Color? activeColor,
  Color? activeTrackColor,
  Color? inactiveThumbColor,
  Color? inactiveTrackColor,
  ImageProvider? activeThumbImage,
  ImageProvider? inactiveThumbImage,
  MouseCursor? mouseCursor,
  bool autofocus = false,
  Color? focusColor,
  Color? hoverColor,
  MaterialTapTargetSize? materialTapTargetSize,
  ListTileControlAffinity controlAffinity = ListTileControlAffinity.trailing,
  EdgeInsetsGeometry? contentPadding,
})
Dart

يوجد أيضاً SwitchListTile.adaptive(...) — يتكيّف شكله مع النظام (مثلاً iOS غالبًا يعطي مظهر مختلف).

أهم الخصائص وشرحها سريعًا
  • value (bool) — الحالة الحالية للمفتاح.
  • onChanged (ValueChanged<bool>?) — تُستدعى عند تغيّر الحالة؛ إذا كانت null يصبح العنصر معطلاً.
  • title / subtitle — نصوص العرض الأساسية.
  • secondary — ويدجت ثانوية (غالبًا Icon أو CircleAvatar) تظهر في طرف العنصر الآخر.
  • controlAffinity — يحدد مكان المفتاح:
    • ListTileControlAffinity.trailing (افتراضي) → المفتاح على الطرف النهائي (يمين في RTL، يسار في LTR حسب الاتجاه).
    • ListTileControlAffinity.leading → المفتاح قبل المحتوى.
    • ListTileControlAffinity.platform → سلوك حسب النظام.
  • activeColor — لون الـ thumb عند التفعيل (يفضل استخدام SwitchTheme للتخصيص الشامل).
  • activeTrackColor — لون مسار المفتاح عند التفعيل.
  • inactiveThumbColor / inactiveTrackColor — الألوان عند التعطيل.
  • activeThumbImage / inactiveThumbImage — صورة توضع فوق الـ thumb.
  • isThreeLine — يسمح بثلاثة أسطر (title + subtitle يمكن أن يلتف لأسطر متعددة).
  • dense — يقلل المسافات داخل الصف.
  • selected — يغيّر نمط النص/أيقونة ليظهر كمحدد.
  • contentPadding — التحكم بهوامش الداخل.
  • autofocus, mouseCursor, focusColor, hoverColor, materialTapTargetSize — لميزات الوصول والسطح/ويب.
سلوك مهم
  • النقر على أي مكان في الصف (ليس فقط على الزر) يغيّر الحالة — طالما onChanged ليس null. هذا يجعل التجربة بديهية للمستخدم.
  • إذا أردت أن يكون فقط الـ switch هو القابل للنقر (وليس الصف كله)، عندها يجب بناء ListTile مخصّص ووضَع Switch في trailing مع منع التبديل عبر نقر الصف.
أمثلة عملية
1 — أبسط استخدام
bool notifications = false;

SwitchListTile(
  title: Text('الإشعارات'),
  subtitle: Text('تشغيل/إيقاف الإشعارات'),
  value: notifications,
  onChanged: (val) => setState(() => notifications = val),
);
Dart
2 — مع أيقونة ثانوية (secondary) والمفتاح على اليسار
SwitchListTile(
  title: Text('الوضع الليلي'),
  secondary: Icon(Icons.brightness_2),
  value: isDark,
  controlAffinity: ListTileControlAffinity.leading,
  onChanged: (v) => setState(() => isDark = v),
);
Dart
3 — تعطيل العنصر
SwitchListTile(
  title: Text('مزامنة الخلفية'),
  value: false,
  onChanged: null, // معطّل
);
Dart
4 — نسخة متكيّفة (تغيّر الشكل عند iOS)
SwitchListTile.adaptive(
  title: Text('مزامنة الصور'),
  value: syncPhotos,
  onChanged: (v) => setState(() => syncPhotos = v),
);
Dart
5 — صورة على الـ thumb
SwitchListTile(
  title: Text('خاصية مع صورة'),
  value: myVal,
  activeThumbImage: AssetImage('assets/on_icon.png'),
  inactiveThumbImage: AssetImage('assets/off_icon.png'),
  onChanged: (v) => setState(() => myVal = v),
);
Dart
6 — التبديل غير متزامن (نمط optimistic + revert on failure)
SwitchListTile(
  title: Text('ميزة سحابة'),
  value: enabled,
  onChanged: (v) async {
    // تحديث واجهة المستخدم مبدئيًا
    setState(() => enabled = v);
    try {
      await myApi.updateFeatureEnabled(v); // نداء الشبكة
    } catch (e) {
      // إذا فشل، ارجع للحالة القديمة وأخبر المستخدم
      setState(() => enabled = !v);
      ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('فشل التحديث')));
    }
  },
);
Dart

التيمينغ (تخصيص مظهر مركزي)
  • لتخصيص ألوان كل الـ Switch في التطبيق:
MaterialApp(
  theme: ThemeData(
    switchTheme: SwitchThemeData(
      thumbColor: MaterialStateProperty.resolveWith((states) {
        if (states.contains(MaterialState.selected)) return Colors.white;
        return Colors.grey.shade200;
      }),
      trackColor: MaterialStateProperty.resolveWith((states) {
        if (states.contains(MaterialState.selected)) return Colors.blue;
        return Colors.grey;
      }),
    ),
    listTileTheme: ListTileThemeData(
      iconColor: Colors.blueGrey,
      textColor: Colors.black87,
    ),
  ),
  home: ...
)
Dart

SwitchListTile يرث مظهر الـ Switch من switchTheme، ونمط النص/أيقونة من listTileTheme.

التكامل مع نماذج الحالة (State management)
  • Provider / Riverpod / Bloc: بدّل value وonChanged لقراءة/تحديث الحالة من الـ provider أو bloc بدلاً من setState.
    مثال بسيط مع Provider:
class SettingsModel with ChangeNotifier {
  bool _notifications = true;
  bool get notifications => _notifications;
  set notifications(bool v) { _notifications = v; notifyListeners(); }
}

// في الواجهة
Consumer<SettingsModel>(
  builder: (_, model, __) => SwitchListTile(
    title: Text('الإشعارات'),
    value: model.notifications,
    onChanged: (v) => model.notifications = v,
  ),
);
Dart
دمجه داخل Form

لا يوجد SwitchListTileFormField مدمج، لكن يمكنك لفه بـ FormField<bool> لتوفر validation وsave:

FormField<bool>(
  initialValue: initialValue,
  builder: (state) {
    return SwitchListTile(
      title: Text('مشاركة الموقع'),
      value: state.value ?? false,
      onChanged: (v) {
        state.didChange(v);
      },
    );
  },
  validator: (val) => val == true ? null : 'يرجى تفعيل المشاركة',
);
Dart
نصائح وملاحظات عملية
  • التفاعل الكلي للصف: قابلية النقر في كامل الصف مفيدة في شاشات الإعداد — لكن إذا كنت تريد زر إعدادات ثانوي قابل للنقر (مثل زر المعلومات) تعامل مع ذلك بشكل منفصل.
  • استخدم .adaptive للمطابقة مع تصاميم النظام (iOS vs Android) بسهولة.
  • الأداء: في قوائم طويلة استخدم ListView.builder وconst حيث أمكن. إن كان لديك كثير من SwitchListTile، حاول أن تجعل كل عنصر const/خالي من الحسابات الثقيلة أو استخدم keys مناسبة لمنع إعادة بناء غير ضرورية.
  • تجنّب عمليات ثقيلة في onChanged مباشرة — إذا كانت تستدعي الشبكة، استعمل نمط optimistic أو أظهر مؤشر تحميل واضح ولا تحظر الواجهة. قد ترد بعمل disable مؤقت للعنصر أثناء العملية.
  • الوصول (Accessibility): title وsubtitle يُقرأان للقارئ الشاشة. إذا تحتاج وصفاً خاصاً أكثر استخدم Semantics حول الـ SwitchListTile لتزويد label أو hint.
  • التحكم في موقع المفتاح عبر controlAffinity إذا أردت المفتاح في اليسار (مثلاً في قائمة RTL خاصة أو لتصميم معين).
  • التخصيص الشامل: لتغيير سلوك التينت في Material 3، انتبه إلى surfaceTintColor وswitchTheme لأن بعض القيم قد تُطبّق افتراضيًا من الثيم.
أخطاء شائعة وحلولها
  • خطأ: لا يتغير الـ Switch عند النقر
    • السبب: onChanged = null أو value لا يُحدّث (نسيت setState أو لم تحدث القيمة في الـ provider).
    • الحل: تأكد أن onChanged يعطي قيمة وأنك تُحدِّث الحالة فعليًا.
  • أريد أن أسمح فقط بالنقر على الـ thumb وليس على الصف
    • الحل: لا تستخدم SwitchListTile مباشرة؛ بدلاً من ذلك اصنع ListTile عادي وضع Switch في trailing مع ضبط onTap الخاص بالـ ListTile إذا أردت سلوك مخصص.
  • التباعد/القياس غير المناسب في القوائم
    • الحل: استخدم dense, contentPadding, وisThreeLine للتحكم بالارتفاع والمسافات.