Radio 

ما هو Radio؟

Radio هو ويدجت لزر اختيار دائري يُستخدم لاختيار قيمة واحدة من مجموعة قيم متنافسة (mutually exclusive). كل مجموعة راديو تملك groupValue مشترك و كل زر له value خاص؛ الذي يساوي groupValue يظهر كـ «محدد».

الفكرة الأساسية (قاعدة العمل)
  • لكل زر: value (قيمة هذا الزر).
  • للمجموعة: groupValue (القيمة المختارة حالياً).
  • عند النقر، يُستدعى onChanged وتمرر القيمة الجديدة — عليك تحديث groupValue لعرض التغيير.

إذا onChanged == null → الزر معطّل (disabled).

البنية الأساسية (مثال بسيط)
import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  String? favoriteColor; // القيمة المختارة

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text("اختيار اللون المفضل")),
        body: Column(
          children: [
            RadioListTile<String>(
              title: Text("أحمر"),
              value: "red",
              groupValue: favoriteColor,
              onChanged: (value) {
                setState(() {
                  favoriteColor = value;
                });
              },
            ),
            RadioListTile<String>(
              title: Text("أخضر"),
              value: "green",
              groupValue: favoriteColor,
              onChanged: (value) {
                setState(() {
                  favoriteColor = value;
                });
              },
            ),
            RadioListTile<String>(
              title: Text("أزرق"),
              value: "blue",
              groupValue: favoriteColor,
              onChanged: (value) {
                setState(() {
                  favoriteColor = value;
                });
              },
            ),
            SizedBox(height: 20),
            Text(
              favoriteColor == null
                  ? "لم تختر أي لون بعد"
                  : "لونك المفضل هو: $favoriteColor",
              style: TextStyle(fontSize: 18),
            ),
          ],
        ),
      ),
    );
  }
}
Dart
// تعريف enum (موصى به بدل أعداد أو نصوص)
enum ColorOption { red, green, blue }

class MyRadioDemo extends StatefulWidget {
  @override
  State<MyRadioDemo> createState() => _MyRadioDemoState();
}

class _MyRadioDemoState extends State<MyRadioDemo> {
  ColorOption? _selected; // nullable مفيد إذا سمحت بإلغاء الاختيار (toggleable)

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Radio<ColorOption>(
          value: ColorOption.red,
          groupValue: _selected,
          onChanged: (ColorOption? v) => setState(() => _selected = v),
        ),
        Radio<ColorOption>(
          value: ColorOption.green,
          groupValue: _selected,
          onChanged: (v) => setState(() => _selected = v),
        ),
        Radio<ColorOption>(
          value: ColorOption.blue,
          groupValue: _selected,
          onChanged: (v) => setState(() => _selected = v),
        ),
      ],
    );
  }
}
Dart
أهم الخصائص وشرحها
  • value (T) — قيمة هذا الراديو.
  • groupValue (T? ) — القيمة الحالية للمجموعة؛ إذا value == groupValue يظهر كمحدد.
  • onChanged (ValueChanged<T?>?) — دالة تُستدعى عند النقر. إذا null يصبح الزر معطّل.
  • toggleable (bool) — إذا true يمكن إلغاء الاختيار بالنقر على الزر المختار (فيتغير onChanged ويمرر null)؛ اجعل state متغيرًا nullable لتدعم هذا.
  • fillColor (MaterialStateProperty<Color?>?) — لون النقطة الداخلية (يُفضّل استخدام MaterialStateProperty.resolveWith لتغيير اللون حسب الحالة مثل selected/disabled).
  • activeColor (Color?) — طريقة قديمة لتلوين؛ الآن يفضل fillColor.
  • splashRadius (double?) — نصف قطر التأثير البصري عند النقر.
  • mouseCursor, focusNode, autofocus, materialTapTargetSize, visualDensity, focusColor, hoverColor — خصائص تحكم التفاعل والولوجية وسلوك الماوس/لوحة المفاتيح.
  • ملاحظة: أسماء بعض الخصائص قد تتغير بإصدارات فلاتر المختلفة — لكن الفكرة العامة نفسها.
استخدام عملي متقدم — RadioListTile

للحصول على صف كامل قابل للنقر مع عنوان ووصف وأيقونة استخدم RadioListTile<T>:

RadioListTile<ColorOption>(
  title: Text('أحمر'),
  subtitle: Text('وصف أحمر'),
  secondary: Icon(Icons.stop),
  value: ColorOption.red,
  groupValue: _selected,
  onChanged: (v) => setState(() => _selected = v),
);
Dart

ميزة RadioListTile: عند النقر في أي مكان في الصف يتغير الاختيار (مساحة نقر أكبر وسهولة استخدام).

تعطيل/تمكين عناصر
  • لتعطيل زر مفرد: اجعل onChanged: null.
  • لتعطيل المجموعة كلها: امسك الحالة في مكان مركزي (مثلاً provider) ومرِّر onChanged: null أو تجاهل التحديثات.
تكامل مع النماذج (Form) و Validation

لا يوجد RadioFormField جاهز، لكن يمكنك استخدام FormField<T>:

FormField<ColorOption>(
  initialValue: ColorOption.red,
  validator: (val) => val == null ? 'اختر خيارًا' : null,
  builder: (state) => Column(
    children: [
      RadioListTile<ColorOption>(
        value: ColorOption.red,
        groupValue: state.value,
        onChanged: (v) => state.didChange(v),
        title: Text('أحمر'),
      ),
      if (state.hasError) Text(state.errorText!, style: TextStyle(color: Colors.red)),
    ],
  ),
)
Dart
التخصيص عبر Theme

لتوحيد مظهر كل الـ Radios في التطبيق:

MaterialApp(
  theme: ThemeData(
    radioTheme: RadioThemeData(
      fillColor: MaterialStateProperty.resolveWith<Color?>((states) {
        if (states.contains(MaterialState.disabled)) return Colors.grey;
        if (states.contains(MaterialState.selected)) return Colors.blue;
        return Colors.grey.shade400;
      }),
      splashRadius: 20,
    ),
  ),
  home: MyHome(),
)
Dart

استخدم MaterialStateProperty لتحديد ألوان مختلفة للحالات: selected, disabled, hovered, pressed, إلخ.

الوصول (Accessibility) و Keyboard
  • استخدم RadioListTile أو غلّف Radio بـSemantics لتزويد تسميات (labels) و hints لقراء الشاشة.
  • عناصر الراديو تدعم التنقل عبر لوحة المفاتيح (arrow keys) عند التركيز ضمن مجموعة، ويمكن استخدام focusNode وautofocus لضبط سلوك التركيز.
مقارنة سريعة مع Checkbox و Switch
  • Radio: اختيار واحد من مجموعة (mutually exclusive).
  • Checkbox: خيارات متعددة ممكن تحديدها أو إزالتها (multi-select).
  • Switch: تبديل حالة ثنائية (on/off) لخاصية واحدة وليس اختيارًا من مجموعة.
نصائح عملية وأخطاء شائعة
  • استخدم enum لقيم value لتكون واضحة ومحمية من الأخطاء بدلاً من أعداد أو سلاسل نصية.
  • اجعل حالة المجموعة nullable (T?) إذا استخدمت toggleable: true (حتى تسمح بإلغاء الاختيار).
  • لا تنس تحديث الـ state داخل onChanged — إذا لم تحدثه لن يرى المستخدم أي تغيير.
  • لبيئة إعدادات استخدم RadioListTile لسهولة الاستخدام ومساحة النقر الكبيرة.
  • لأداء قوائم طويلة استخدم ListView.builder ولا تعيد بناء كامل الواجهة عند اختيار عنصر—فكّر في فصل كل صف إلى ويدجت مستقل باستخدام const و ValueKey عند الحاجة.
  • عند الحاجة لتدرّج أو خلفيات مخصصة غلّف الـ Radio بحاوية أو استخدم RadioListTile داخل Card مع shape وclipBehavior.
مثال متكامل (enum + toggleable + RadioListTile)
enum Gender { male, female, other }

class GenderSelector extends StatefulWidget {
  @override
  State<GenderSelector> createState() => _GenderSelectorState();
}

class _GenderSelectorState extends State<GenderSelector> {
  Gender? _gender; // nullable لدعم toggleable

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        RadioListTile<Gender>(
          title: Text('ذكر'),
          value: Gender.male,
          groupValue: _gender,
          onChanged: (g) => setState(() => _gender = g),
        ),
        RadioListTile<Gender>(
          title: Text('أنثى'),
          value: Gender.female,
          groupValue: _gender,
          onChanged: (g) => setState(() => _gender = g),
        ),
        RadioListTile<Gender>(
          title: Text('آخر'),
          value: Gender.other,
          groupValue: _gender,
          onChanged: (g) => setState(() => _gender = g),
        ),
        // مثال على toggleable:
        Radio<Gender>(
          value: Gender.male,
          groupValue: _gender,
          toggleable: true,
          onChanged: (g) => setState(() => _gender = g),
        ),
      ],
    );
  }
}
Dart
خلاصة سريعة
  • استخدم Radio لاختيار قيمة واحدة من مجموعة.
  • إدارة الحالة عبر groupValue وonChanged.
  • استخدم RadioListTile لواجهة مستخدم أفضل ومساحة نقر أكبر.
  • خصّص الألوان عبر radioTheme أو fillColor وMaterialStateProperty.
  • احرص على الوصول (Semantics) وتحديث الحالة بشكل صحيح.
📌 ما هو RadioListTile؟

هو ويدجت جاهزة في Flutter توفر:

  • زر Radio (زر اختيار دائري).
  • عنوان (title).
  • وصف (subtitle) اختياري.
  • أيقونة جانبية (secondary) اختيارية.
  • إمكانية النقر على أي مكان في الصف لتغيير الاختيار، مش بس على الدائرة.

يعني هو مخصص لعمل قوائم خيارات (مثل إعدادات اختيار اللغة أو الجنس) بطريقة منظمة وأنيقة.

🔹 البنية الأساسية
RadioListTile<T>(
  value: ... ,       // قيمة هذا العنصر
  groupValue: ... ,  // القيمة المحددة حاليًا للمجموعة
  onChanged: ... ,   // ماذا يحدث عند التغيير
  title: ... ,       // العنوان
  subtitle: ... ,    // وصف فرعي (اختياري)
  secondary: ... ,   // أيقونة أو ويدجت جانبية (اختياري)
)
Dart
⚙️ أهم الخصائص
الخاصيةالنوعالوصف
valueTالقيمة التي يمثلها هذا الخيار
groupValueT?القيمة الحالية للمجموعة (القيمة المختارة)
onChangedValueChanged<T?>?الدالة التي تنفذ عند النقر أو التغيير
titleWidgetعنوان الخيار
subtitleWidget?نص فرعي (مثلاً وصف)
secondaryWidget?أيقونة أو صورة على الجهة الأخرى
toggleableboolإذا كان يمكن إلغاء التحديد (مثل زر On/Off)
activeColorColor?لون دائرة الراديو عند التحديد (قد يُستبدل بـ fillColor)
fillColorMaterialStateProperty<Color?>?طريقة حديثة لتحديد ألوان الراديو بناءً على الحالة
controlAffinityListTileControlAffinityموقع دائرة الراديو (leading أو trailing أو platform default)
contentPaddingEdgeInsetsGeometry?المسافة الداخلية حول المحتوى
selectedboolهل يظهر الصف كمحدد بصريًا (Bold أو لون مختلف)
selectedTileColorColor?لون الخلفية عند التحديد
tileColorColor?لون الخلفية العادي
densebool?لتقليل حجم المسافات (شكل مضغوط)
visualDensityVisualDensity?تحكم في كثافة العرض
shapeShapeBorder?شكل الحواف (مربعة، مستديرة…)
isThreeLineboolإذا كان لديك عنوان وسطرين من النصوص
🖼 مثال بسيط
enum Gender { male, female }

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  Gender? _selectedGender;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        RadioListTile<Gender>(
          title: Text("ذكر"),
          value: Gender.male,
          groupValue: _selectedGender,
          onChanged: (Gender? value) {
            setState(() {
              _selectedGender = value;
            });
          },
        ),
        RadioListTile<Gender>(
          title: Text("أنثى"),
          value: Gender.female,
          groupValue: _selectedGender,
          onChanged: (value) {
            setState(() {
              _selectedGender = value;
            });
          },
        ),
      ],
    );
  }
}
Dart
🎨 تخصيص الألوان والمظهر
RadioListTile<String>(
  title: Text("الخيار الأول"),
  subtitle: Text("وصف قصير"),
  secondary: Icon(Icons.star),
  value: "option1",
  groupValue: selectedOption,
  onChanged: (value) => setState(() => selectedOption = value),
  activeColor: Colors.pink, // لون الدائرة عند التحديد
  tileColor: Colors.grey.shade100, // لون الخلفية
  selectedTileColor: Colors.pink.shade50, // خلفية عند التحديد
  controlAffinity: ListTileControlAffinity.trailing, // ضع الدائرة على اليمين
)
Dart
📱 استخداماته الشائعة
  1. اختيار نوع من عدة خيارات (الجنس، اللغة، الوضع الليلي…).
  2. عمل قوائم إعدادات تفاعلية.
  3. استبدال Radio + ListTile بكود أقصر وأكثر ترتيبًا.
📝 نصائح
  • استخدم enum بدل النصوص لتجنب الأخطاء في القيم.
  • إذا كنت تريد أن يتمكن المستخدم من إلغاء التحديد، استخدم toggleable: true واجعل groupValue قابل لأن يكون null.
  • إذا لديك قائمة طويلة من الخيارات، ضعها داخل ListView لتجنب مشاكل التمرير.