أساسيات ودجات Flutter

Switch و Slider و Radio

45 دقيقة الدرس 10 من 18

ودجة Switch

ودجة Switch هي مفتاح تبديل Material Design يسمح للمستخدمين بتشغيل أو إيقاف إعداد ما. يُستخدم عادةً للتفضيلات البوليانية مثل تفعيل الوضع الداكن أو الإشعارات أو الواي فاي. للمفتاح حالتان: تشغيل (true) وإيقاف (false).

Switch أساسي

class SwitchDemo extends StatefulWidget {
  const SwitchDemo({super.key});

  @override
  State<SwitchDemo> createState() => _SwitchDemoState();
}

class _SwitchDemoState extends State<SwitchDemo> {
  bool _isEnabled = false;

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceBetween,
      children: [
        const Text('تفعيل الميزة'),
        Switch(
          value: _isEnabled,
          onChanged: (bool value) {
            setState(() {
              _isEnabled = value;
            });
          },
        ),
      ],
    );
  }
}

خصائص Switch الرئيسية:

  • value -- هل المفتاح مفعّل (true) أو معطّل (false).
  • onChanged -- دالة رد الاتصال عندما يبدّل المستخدم المفتاح. اضبطها على null للتعطيل.
  • activeColor -- لون الإبهام عندما يكون المفتاح مفعّلاً.
  • activeTrackColor -- لون المسار عندما يكون المفتاح مفعّلاً.
  • inactiveThumbColor -- لون الإبهام عندما يكون معطّلاً.
  • inactiveTrackColor -- لون المسار عندما يكون معطّلاً.
  • thumbIcon -- أيقونة تُعرض على إبهام المفتاح (Material 3).

SwitchListTile

SwitchListTile يجمع بين Switch و ListTile، ويوفر منطقة قابلة للضغط بعرض كامل. هذه هي الطريقة المفضلة لاستخدام المفاتيح في شاشات الإعدادات.

أمثلة SwitchListTile

class NotificationSettings extends StatefulWidget {
  const NotificationSettings({super.key});

  @override
  State<NotificationSettings> createState() =>
      _NotificationSettingsState();
}

class _NotificationSettingsState
    extends State<NotificationSettings> {
  bool _pushNotifications = true;
  bool _emailNotifications = false;
  bool _soundEnabled = true;

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.all(16),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          SwitchListTile(
            title: const Text('إشعارات الدفع'),
            subtitle: const Text('تلقي تنبيهات الدفع'),
            secondary: const Icon(Icons.notifications),
            value: _pushNotifications,
            onChanged: (value) {
              setState(() => _pushNotifications = value);
            },
          ),
          const Divider(height: 1),
          SwitchListTile(
            title: const Text('إشعارات البريد'),
            subtitle: const Text('الحصول على تحديثات عبر البريد'),
            secondary: const Icon(Icons.email),
            value: _emailNotifications,
            onChanged: (value) {
              setState(() => _emailNotifications = value);
            },
          ),
          const Divider(height: 1),
          SwitchListTile(
            title: const Text('الصوت'),
            subtitle: const Text('تشغيل أصوات الإشعارات'),
            secondary: const Icon(Icons.volume_up),
            value: _soundEnabled,
            onChanged: _pushNotifications
                ? (value) {
                    setState(() => _soundEnabled = value);
                  }
                : null, // معطّل عندما يكون الدفع مغلقاً
          ),
        ],
      ),
    );
  }
}
نصيحة: عطّل الإعدادات التابعة عندما يكون إعدادها الأساسي مغلقاً. في المثال أعلاه، مفتاح الصوت معطّل عندما تكون إشعارات الدفع مغلقة. هذا يوفر تلميحاً بصرياً واضحاً حول التبعية بين الإعدادات.

مفتاح مخصص المظهر

يمكنك تخصيص مظهر Switch باستخدام خصائص اللون والأيقونة:

مفتاح منسق مع أيقونة الإبهام

Switch(
  value: _isEnabled,
  onChanged: (value) {
    setState(() => _isEnabled = value);
  },
  activeColor: Colors.green,
  activeTrackColor: Colors.green[200],
  inactiveThumbColor: Colors.grey,
  inactiveTrackColor: Colors.grey[300],
  thumbIcon: WidgetStateProperty.resolveWith<Icon?>(
    (Set<WidgetState> states) {
      if (states.contains(WidgetState.selected)) {
        return const Icon(Icons.check, color: Colors.white);
      }
      return const Icon(Icons.close, color: Colors.white);
    },
  ),
)

CupertinoSwitch

لمفتاح بنمط iOS، استخدم CupertinoSwitch من مكتبة Cupertino:

CupertinoSwitch

import 'package:flutter/cupertino.dart';

CupertinoSwitch(
  value: _isEnabled,
  activeColor: CupertinoColors.activeGreen,
  onChanged: (bool value) {
    setState(() {
      _isEnabled = value;
    });
  },
)

ودجة Slider

ودجة Slider تتيح للمستخدمين اختيار قيمة من نطاق مستمر أو منفصل عن طريق سحب إبهام على طول مسار. الاستخدامات الشائعة تشمل أدوات التحكم في الصوت وإعدادات السطوع ونطاقات الفلتر.

Slider أساسي

class SliderDemo extends StatefulWidget {
  const SliderDemo({super.key});

  @override
  State<SliderDemo> createState() => _SliderDemoState();
}

class _SliderDemoState extends State<SliderDemo> {
  double _volume = 50;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(
          'مستوى الصوت: \${_volume.round()}',
          style: const TextStyle(fontSize: 18),
        ),
        Slider(
          value: _volume,
          min: 0,
          max: 100,
          onChanged: (double value) {
            setState(() {
              _volume = value;
            });
          },
        ),
      ],
    );
  }
}

خصائص Slider الرئيسية:

  • value -- القيمة الحالية للمنزلق.
  • min / max -- القيم الدنيا والقصوى (الافتراضي: 0.0 و 1.0).
  • divisions -- عدد التقسيمات المنفصلة. إذا كان null، يكون المنزلق مستمراً.
  • label -- تسمية تُعرض فوق الإبهام عند استخدام التقسيمات.
  • onChanged -- تُستدعى باستمرار أثناء سحب المستخدم للمنزلق.
  • onChangeStart -- تُستدعى عندما يبدأ المستخدم السحب.
  • onChangeEnd -- تُستدعى عندما يتوقف المستخدم عن السحب.
  • activeColor -- لون الجزء النشط من المسار (قبل الإبهام).
  • inactiveColor -- لون الجزء غير النشط (بعد الإبهام).

منزلق منفصل مع تقسيمات

إضافة divisions تجعل المنزلق ينتقل إلى قيم منفصلة ويظهر مؤشر قيمة:

منزلق منفصل

class FontSizeSlider extends StatefulWidget {
  const FontSizeSlider({super.key});

  @override
  State<FontSizeSlider> createState() => _FontSizeSliderState();
}

class _FontSizeSliderState extends State<FontSizeSlider> {
  double _fontSize = 16;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text(
          'نص معاينة',
          style: TextStyle(fontSize: _fontSize),
        ),
        const SizedBox(height: 16),
        Slider(
          value: _fontSize,
          min: 10,
          max: 36,
          divisions: 26,
          label: '\${_fontSize.round()} نقطة',
          onChanged: (double value) {
            setState(() {
              _fontSize = value;
            });
          },
        ),
        Text('حجم الخط: \${_fontSize.round()} نقطة'),
      ],
    );
  }
}
ملاحظة: خاصية label تظهر فقط عند تعيين divisions. تعرض مؤشراً عائماً فوق الإبهام يعرض القيمة الحالية، وهو مفيد جداً للاختيار المنفصل.

RangeSlider

RangeSlider يسمح للمستخدمين باختيار نطاق من القيم عبر توفير إبهامين:

مثال RangeSlider

class PriceRangeFilter extends StatefulWidget {
  const PriceRangeFilter({super.key});

  @override
  State<PriceRangeFilter> createState() =>
      _PriceRangeFilterState();
}

class _PriceRangeFilterState extends State<PriceRangeFilter> {
  RangeValues _priceRange = const RangeValues(20, 80);

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          'السعر: \$\${_priceRange.start.round()} - '
          '\$\${_priceRange.end.round()}',
          style: const TextStyle(
            fontSize: 18,
            fontWeight: FontWeight.bold,
          ),
        ),
        RangeSlider(
          values: _priceRange,
          min: 0,
          max: 100,
          divisions: 20,
          labels: RangeLabels(
            '\$\${_priceRange.start.round()}',
            '\$\${_priceRange.end.round()}',
          ),
          onChanged: (RangeValues values) {
            setState(() {
              _priceRange = values;
            });
          },
        ),
      ],
    );
  }
}

CupertinoSlider

لمنزلق بنمط iOS، استخدم CupertinoSlider:

CupertinoSlider

import 'package:flutter/cupertino.dart';

CupertinoSlider(
  value: _volume,
  min: 0,
  max: 100,
  divisions: 100,
  onChanged: (double value) {
    setState(() {
      _volume = value;
    });
  },
)

ودجة Radio

ودجة Radio تسمح للمستخدمين باختيار خيار واحد من مجموعة خيارات متنافية. جميع أزرار Radio في مجموعة تشارك نفس groupValue، ويمكن تحديد واحد فقط في كل مرة.

مجموعة Radio أساسية

class GenderSelection extends StatefulWidget {
  const GenderSelection({super.key});

  @override
  State<GenderSelection> createState() => _GenderSelectionState();
}

class _GenderSelectionState extends State<GenderSelection> {
  String _selectedGender = 'male';

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        const Text(
          'الجنس',
          style: TextStyle(
            fontSize: 16,
            fontWeight: FontWeight.bold,
          ),
        ),
        Row(
          children: [
            Radio<String>(
              value: 'male',
              groupValue: _selectedGender,
              onChanged: (String? value) {
                setState(() {
                  _selectedGender = value!;
                });
              },
            ),
            const Text('ذكر'),
            Radio<String>(
              value: 'female',
              groupValue: _selectedGender,
              onChanged: (String? value) {
                setState(() {
                  _selectedGender = value!;
                });
              },
            ),
            const Text('أنثى'),
            Radio<String>(
              value: 'other',
              groupValue: _selectedGender,
              onChanged: (String? value) {
                setState(() {
                  _selectedGender = value!;
                });
              },
            ),
            const Text('آخر'),
          ],
        ),
      ],
    );
  }
}

خصائص Radio الرئيسية:

  • value -- القيمة التي يمثلها هذا Radio.
  • groupValue -- القيمة المحددة حالياً في المجموعة. عندما value == groupValue، يكون هذا Radio محدداً.
  • onChanged -- دالة رد الاتصال عند تحديد هذا Radio.
  • activeColor -- اللون عند التحديد.
  • toggleable -- إذا كان true، الضغط على Radio المحدد يلغي تحديده (يضبط groupValue على null).

RadioListTile

RadioListTile يجمع بين زر Radio و ListTile لمنطقة ضغط أكبر وتخطيط أفضل:

مثال RadioListTile

class ThemeSelector extends StatefulWidget {
  const ThemeSelector({super.key});

  @override
  State<ThemeSelector> createState() => _ThemeSelectorState();
}

class _ThemeSelectorState extends State<ThemeSelector> {
  String _selectedTheme = 'system';

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.all(16),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          const Padding(
            padding: EdgeInsets.all(16),
            child: Text(
              'سمة التطبيق',
              style: TextStyle(
                fontSize: 18,
                fontWeight: FontWeight.bold,
              ),
            ),
          ),
          RadioListTile<String>(
            title: const Text('فاتح'),
            subtitle: const Text('استخدام السمة الفاتحة دائماً'),
            secondary: const Icon(Icons.light_mode),
            value: 'light',
            groupValue: _selectedTheme,
            onChanged: (value) {
              setState(() => _selectedTheme = value!);
            },
          ),
          RadioListTile<String>(
            title: const Text('داكن'),
            subtitle: const Text('استخدام السمة الداكنة دائماً'),
            secondary: const Icon(Icons.dark_mode),
            value: 'dark',
            groupValue: _selectedTheme,
            onChanged: (value) {
              setState(() => _selectedTheme = value!);
            },
          ),
          RadioListTile<String>(
            title: const Text('النظام'),
            subtitle: const Text('اتباع إعداد الجهاز'),
            secondary: const Icon(Icons.settings_suggest),
            value: 'system',
            groupValue: _selectedTheme,
            onChanged: (value) {
              setState(() => _selectedTheme = value!);
            },
          ),
        ],
      ),
    );
  }
}

استخدام Enums مع Radio

استخدام enums مع ودجات Radio هو نهج أنظف وآمن من حيث النوع:

Radio مع Enums

enum ShippingMethod { standard, express, overnight }

class ShippingSelector extends StatefulWidget {
  const ShippingSelector({super.key});

  @override
  State<ShippingSelector> createState() =>
      _ShippingSelectorState();
}

class _ShippingSelectorState extends State<ShippingSelector> {
  ShippingMethod _method = ShippingMethod.standard;

  String _getLabel(ShippingMethod method) {
    switch (method) {
      case ShippingMethod.standard:
        return 'عادي (5-7 أيام)';
      case ShippingMethod.express:
        return 'سريع (2-3 أيام)';
      case ShippingMethod.overnight:
        return 'بين ليلة وضحاها (اليوم التالي)';
    }
  }

  String _getPrice(ShippingMethod method) {
    switch (method) {
      case ShippingMethod.standard:
        return 'مجاني';
      case ShippingMethod.express:
        return '\$9.99';
      case ShippingMethod.overnight:
        return '\$19.99';
    }
  }

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.all(16),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: ShippingMethod.values.map((method) {
          return RadioListTile<ShippingMethod>(
            title: Text(_getLabel(method)),
            subtitle: Text(_getPrice(method)),
            value: method,
            groupValue: _method,
            onChanged: (value) {
              setState(() => _method = value!);
            },
          );
        }).toList(),
      ),
    );
  }
}
نصيحة: استخدم دائماً enums بدلاً من النصوص لقيم مجموعة Radio. توفر Enums أماناً في وقت الترجمة وتمنع الأخطاء الإملائية وتجعل الكود أسهل في إعادة البناء. يمكن لـ Dart enums أيضاً أن تحتوي على دوال وخصائص (enums محسّنة) لكود أنظف.

مثال عملي: صفحة الإعدادات

صفحة إعدادات كاملة

class SettingsPage extends StatefulWidget {
  const SettingsPage({super.key});

  @override
  State<SettingsPage> createState() => _SettingsPageState();
}

class _SettingsPageState extends State<SettingsPage> {
  bool _darkMode = false;
  bool _notifications = true;
  double _fontSize = 16;
  String _language = 'en';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('الإعدادات')),
      body: ListView(
        children: [
          // قسم المفاتيح
          const _SectionHeader(title: 'عام'),
          SwitchListTile(
            secondary: const Icon(Icons.dark_mode),
            title: const Text('الوضع الداكن'),
            value: _darkMode,
            onChanged: (v) => setState(() => _darkMode = v),
          ),
          SwitchListTile(
            secondary: const Icon(Icons.notifications),
            title: const Text('الإشعارات'),
            value: _notifications,
            onChanged: (v) => setState(() => _notifications = v),
          ),

          // قسم المنزلق
          const _SectionHeader(title: 'العرض'),
          ListTile(
            leading: const Icon(Icons.text_fields),
            title: const Text('حجم الخط'),
            subtitle: Slider(
              value: _fontSize,
              min: 12,
              max: 24,
              divisions: 6,
              label: '\${_fontSize.round()} نقطة',
              onChanged: (v) => setState(() => _fontSize = v),
            ),
            trailing: Text('\${_fontSize.round()}'),
          ),

          // قسم الراديو
          const _SectionHeader(title: 'اللغة'),
          RadioListTile<String>(
            title: const Text('الإنجليزية'),
            value: 'en',
            groupValue: _language,
            onChanged: (v) => setState(() => _language = v!),
          ),
          RadioListTile<String>(
            title: const Text('العربية'),
            value: 'ar',
            groupValue: _language,
            onChanged: (v) => setState(() => _language = v!),
          ),
          RadioListTile<String>(
            title: const Text('الإسبانية'),
            value: 'es',
            groupValue: _language,
            onChanged: (v) => setState(() => _language = v!),
          ),
        ],
      ),
    );
  }
}

class _SectionHeader extends StatelessWidget {
  final String title;
  const _SectionHeader({required this.title});

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.fromLTRB(16, 24, 16, 8),
      child: Text(
        title,
        style: TextStyle(
          fontSize: 14,
          fontWeight: FontWeight.bold,
          color: Colors.grey[600],
        ),
      ),
    );
  }
}

مثال عملي: أدوات تحكم الفلتر

لوحة فلتر المنتجات

class FilterPanel extends StatefulWidget {
  const FilterPanel({super.key});

  @override
  State<FilterPanel> createState() => _FilterPanelState();
}

class _FilterPanelState extends State<FilterPanel> {
  RangeValues _priceRange = const RangeValues(10, 90);
  double _rating = 3;
  String _sortBy = 'relevance';
  bool _inStock = true;
  bool _onSale = false;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('الفلاتر')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          // نطاق السعر
          const Text(
            'نطاق السعر',
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
          Text(
            '\$\${_priceRange.start.round()} - '
            '\$\${_priceRange.end.round()}',
          ),
          RangeSlider(
            values: _priceRange,
            min: 0,
            max: 100,
            divisions: 20,
            labels: RangeLabels(
              '\$\${_priceRange.start.round()}',
              '\$\${_priceRange.end.round()}',
            ),
            onChanged: (v) => setState(() => _priceRange = v),
          ),
          const SizedBox(height: 16),

          // أدنى تقييم
          const Text(
            'أدنى تقييم',
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
          Row(
            children: [
              Expanded(
                child: Slider(
                  value: _rating,
                  min: 1,
                  max: 5,
                  divisions: 4,
                  label: '\${_rating.round()} نجوم',
                  onChanged: (v) => setState(() => _rating = v),
                ),
              ),
              Text('\${_rating.round()} نجوم'),
            ],
          ),
          const SizedBox(height: 16),

          // خيارات الترتيب
          const Text(
            'ترتيب حسب',
            style: TextStyle(fontWeight: FontWeight.bold),
          ),
          RadioListTile<String>(
            title: const Text('الصلة'),
            value: 'relevance',
            groupValue: _sortBy,
            onChanged: (v) => setState(() => _sortBy = v!),
            dense: true,
          ),
          RadioListTile<String>(
            title: const Text('السعر: من الأقل للأعلى'),
            value: 'price_asc',
            groupValue: _sortBy,
            onChanged: (v) => setState(() => _sortBy = v!),
            dense: true,
          ),
          RadioListTile<String>(
            title: const Text('السعر: من الأعلى للأقل'),
            value: 'price_desc',
            groupValue: _sortBy,
            onChanged: (v) => setState(() => _sortBy = v!),
            dense: true,
          ),
          const SizedBox(height: 16),

          // فلاتر التبديل
          SwitchListTile(
            title: const Text('متوفر فقط'),
            value: _inStock,
            onChanged: (v) => setState(() => _inStock = v),
          ),
          SwitchListTile(
            title: const Text('عروض خاصة'),
            value: _onSale,
            onChanged: (v) => setState(() => _onSale = v),
          ),
        ],
      ),
      bottomNavigationBar: Padding(
        padding: const EdgeInsets.all(16),
        child: ElevatedButton(
          onPressed: () {
            debugPrint('تطبيق الفلاتر...');
          },
          style: ElevatedButton.styleFrom(
            padding: const EdgeInsets.symmetric(vertical: 16),
          ),
          child: const Text('تطبيق الفلاتر'),
        ),
      ),
    );
  }
}
تحذير: عند استخدام Slider داخل Row أفقي أو عرض محدد، غلّفه دائماً بـ Expanded أو أعطه عرضاً ثابتاً. بدون قيود، سيحاول Slider أخذ عرض لا نهائي ويسبب خطأ تخطيط.

مثال عملي: نموذج التفضيلات

نموذج تفضيلات المستخدم

class PreferenceForm extends StatefulWidget {
  const PreferenceForm({super.key});

  @override
  State<PreferenceForm> createState() => _PreferenceFormState();
}

class _PreferenceFormState extends State<PreferenceForm> {
  bool _newsletter = true;
  bool _autoplay = false;
  double _brightness = 0.7;
  String _quality = 'hd';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('التفضيلات')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          Card(
            child: Column(
              children: [
                SwitchListTile(
                  secondary: const Icon(Icons.mail),
                  title: const Text('النشرة الإخبارية'),
                  subtitle: const Text('تحديثات أسبوعية'),
                  value: _newsletter,
                  onChanged: (v) =>
                      setState(() => _newsletter = v),
                ),
                const Divider(height: 1),
                SwitchListTile(
                  secondary: const Icon(Icons.play_arrow),
                  title: const Text('تشغيل تلقائي للفيديو'),
                  value: _autoplay,
                  onChanged: (v) =>
                      setState(() => _autoplay = v),
                ),
              ],
            ),
          ),
          const SizedBox(height: 16),
          Card(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Row(
                    mainAxisAlignment:
                        MainAxisAlignment.spaceBetween,
                    children: [
                      const Text(
                        'السطوع',
                        style: TextStyle(
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                      Text(
                        '\${(_brightness * 100).round()}%',
                      ),
                    ],
                  ),
                  Slider(
                    value: _brightness,
                    onChanged: (v) =>
                        setState(() => _brightness = v),
                  ),
                ],
              ),
            ),
          ),
          const SizedBox(height: 16),
          Card(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const Padding(
                  padding: EdgeInsets.all(16),
                  child: Text(
                    'جودة الفيديو',
                    style: TextStyle(
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
                RadioListTile<String>(
                  title: const Text('تلقائي'),
                  subtitle: const Text(
                      'يتكيف بناءً على الاتصال'),
                  value: 'auto',
                  groupValue: _quality,
                  onChanged: (v) =>
                      setState(() => _quality = v!),
                ),
                RadioListTile<String>(
                  title: const Text('HD (720p)'),
                  value: 'hd',
                  groupValue: _quality,
                  onChanged: (v) =>
                      setState(() => _quality = v!),
                ),
                RadioListTile<String>(
                  title: const Text('Full HD (1080p)'),
                  value: 'fhd',
                  groupValue: _quality,
                  onChanged: (v) =>
                      setState(() => _quality = v!),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

تمرين عملي

ابنِ شاشة تفضيلات كاملة بجميع أنواع الودجات الثلاثة: (1) قسم Switch بثلاثة SwitchListTiles حيث يكون المفتاحان الثاني والثالث معطلين عندما يكون الأول مغلقاً. (2) قسم Slider بمنزلق عادي (لقيمة واحدة مثل مستوى الصوت) و RangeSlider (لنطاق مثل فلتر السعر)، كلاهما بتقسيمات وتسميات. (3) قسم Radio باستخدام enums مع RadioListTile لاختيار سمة (فاتح، داكن، النظام). (4) زر "تطبيق" في الأسفل يطبع جميع القيم الحالية. التحدي: استخدم CupertinoSwitch لمفتاح واحد وأضف منزلقاً مخصص المظهر مع SliderTheme.