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

ودجت الأزرار

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

ودجت الأزرار في Flutter

الأزرار هي الطريقة الأساسية لتفاعل المستخدمين مع تطبيقك. يوفر Flutter مجموعة غنية من ودجت أزرار Material Design، كل منها مصمم لحالات استخدام محددة. في Flutter 3.x، نظام الأزرار مبني حول ثلاثة أزرار أساسية (ElevatedButton، TextButton، OutlinedButton) مع تنسيق متسق عبر ButtonStyle.

ElevatedButton

ElevatedButton هو زر مملوء مع ارتفاع (ظل). هو أبرز نوع أزرار ويُستخدم للإجراءات الأساسية التي تريد أن يلاحظها المستخدمون أولاً.

أمثلة على ElevatedButton

// زر مرتفع أساسي
ElevatedButton(
  onPressed: () {
    debugPrint('تم الضغط على الزر!');
  },
  child: const Text('إرسال'),
)

// مع أيقونة
ElevatedButton.icon(
  onPressed: () {},
  icon: const Icon(Icons.send),
  label: const Text('إرسال رسالة'),
)

// زر مرتفع منسق
ElevatedButton(
  onPressed: () {},
  style: ElevatedButton.styleFrom(
    backgroundColor: Colors.blue,
    foregroundColor: Colors.white,
    padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(12),
    ),
    elevation: 4,
    textStyle: const TextStyle(
      fontSize: 16,
      fontWeight: FontWeight.w600,
    ),
  ),
  child: const Text('ابدأ الآن'),
)

// زر بعرض كامل
SizedBox(
  width: double.infinity,
  height: 52,
  child: ElevatedButton(
    onPressed: () {},
    child: const Text('متابعة'),
  ),
)
ملاحظة: في Material 3 (الافتراضي في Flutter 3.x)، يحتوي ElevatedButton على نظام ألوان نغمي بشكل افتراضي. استخدم ElevatedButton.styleFrom() لتخصيص الألوان. الـ RaisedButton القديم مهمل — استخدم دائماً ElevatedButton بدلاً منه.

TextButton

TextButton هو زر مسطح بدون ارتفاع أو حدود. يُستخدم للإجراءات الأقل بروزاً، خاصة في مربعات الحوار والبطاقات وأشرطة الأدوات حيث يكون المظهر الأكثر دقة مناسباً.

أمثلة على TextButton

// زر نص أساسي
TextButton(
  onPressed: () {},
  child: const Text('اعرف المزيد'),
)

// مع أيقونة
TextButton.icon(
  onPressed: () {},
  icon: const Icon(Icons.arrow_forward),
  label: const Text('التالي'),
)

// زر نص منسق
TextButton(
  onPressed: () {},
  style: TextButton.styleFrom(
    foregroundColor: Colors.red,
    textStyle: const TextStyle(
      fontSize: 16,
      fontWeight: FontWeight.w500,
    ),
    padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
  ),
  child: const Text('حذف الحساب'),
)

// في مربع حوار
AlertDialog(
  title: const Text('تأكيد'),
  content: const Text('هل أنت متأكد أنك تريد المتابعة؟'),
  actions: [
    TextButton(
      onPressed: () => Navigator.pop(context),
      child: const Text('إلغاء'),
    ),
    TextButton(
      onPressed: () {
        // إجراء التأكيد
        Navigator.pop(context);
      },
      child: const Text('تأكيد'),
    ),
  ],
)

OutlinedButton

OutlinedButton له حدود لكن بدون لون تعبئة وبدون ارتفاع. يقع بين ElevatedButton (تأكيد عالي) وTextButton (تأكيد منخفض) من حيث البروز البصري. استخدمه للإجراءات الثانوية.

أمثلة على OutlinedButton

// زر محدد أساسي
OutlinedButton(
  onPressed: () {},
  child: const Text('عرض التفاصيل'),
)

// مع أيقونة
OutlinedButton.icon(
  onPressed: () {},
  icon: const Icon(Icons.download),
  label: const Text('تحميل'),
)

// منسق مخصص
OutlinedButton(
  onPressed: () {},
  style: OutlinedButton.styleFrom(
    foregroundColor: Colors.green,
    side: const BorderSide(color: Colors.green, width: 2),
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(20),
    ),
    padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 14),
  ),
  child: const Text('أضف للسلة'),
)

IconButton

تناولنا IconButton في الدرس السابق لتفاعلات الأيقونات. إليك ملخص سريع مع أنماط تنسيق إضافية متاحة في Material 3:

متغيرات IconButton (Material 3)

Row(
  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  children: [
    // قياسي
    IconButton(
      onPressed: () {},
      icon: const Icon(Icons.edit),
      tooltip: 'تعديل',
    ),

    // مملوء
    IconButton.filled(
      onPressed: () {},
      icon: const Icon(Icons.add),
    ),

    // مملوء نغمي
    IconButton.filledTonal(
      onPressed: () {},
      icon: const Icon(Icons.bookmark),
    ),

    // محدد
    IconButton.outlined(
      onPressed: () {},
      icon: const Icon(Icons.share),
    ),
  ],
)

FloatingActionButton

FloatingActionButton (FAB) هو زر دائري يطفو فوق المحتوى. يمثل الإجراء الأساسي للشاشة. استخدمه باعتدال — عادة واحد لكل شاشة.

أمثلة على FloatingActionButton

// في Scaffold
Scaffold(
  appBar: AppBar(title: const Text('عرض FAB')),
  body: const Center(child: Text('المحتوى')),

  // FAB دائري قياسي
  floatingActionButton: FloatingActionButton(
    onPressed: () {},
    tooltip: 'إضافة',
    child: const Icon(Icons.add),
  ),
)

// FAB صغير
FloatingActionButton.small(
  onPressed: () {},
  child: const Icon(Icons.add),
)

// FAB كبير
FloatingActionButton.large(
  onPressed: () {},
  child: const Icon(Icons.add),
)

// FAB ممتد مع تسمية
FloatingActionButton.extended(
  onPressed: () {},
  icon: const Icon(Icons.add),
  label: const Text('منشور جديد'),
)

// FAB منسق مخصص
FloatingActionButton(
  onPressed: () {},
  backgroundColor: Colors.deepPurple,
  foregroundColor: Colors.white,
  shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(16),
  ),
  child: const Icon(Icons.chat),
)
نصيحة: FAB الممتد رائع للشاشات حيث قد لا يكون الإجراء الأساسي واضحاً من مجرد أيقونة. ادمج FloatingActionButton.extended مع floatingActionButtonLocation على Scaffold للتحكم في موقعه.

DropdownButton

DropdownButton يتيح للمستخدمين اختيار قيمة واحدة من قائمة خيارات. يعرض القيمة المحددة حالياً ويفتح قائمة منسدلة عند النقر.

مثال على DropdownButton

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

  @override
  State<LanguageSelector> createState() => _LanguageSelectorState();
}

class _LanguageSelectorState extends State<LanguageSelector> {
  String _selectedLanguage = 'العربية';

  final List<String> _languages = [
    'الإنجليزية', 'العربية', 'الإسبانية', 'الفرنسية', 'الألمانية',
  ];

  @override
  Widget build(BuildContext context) {
    return DropdownButton<String>(
      value: _selectedLanguage,
      icon: const Icon(Icons.arrow_drop_down),
      isExpanded: true,
      underline: Container(height: 2, color: Colors.blue),
      items: _languages.map((lang) => DropdownMenuItem(
        value: lang,
        child: Text(lang),
      )).toList(),
      onChanged: (value) {
        if (value != null) {
          setState(() => _selectedLanguage = value);
        }
      },
    );
  }
}

// DropdownButtonFormField للنماذج
DropdownButtonFormField<String>(
  value: _selectedLanguage,
  decoration: const InputDecoration(
    labelText: 'اللغة',
    border: OutlineInputBorder(),
    prefixIcon: Icon(Icons.language),
  ),
  items: _languages.map((lang) => DropdownMenuItem(
    value: lang,
    child: Text(lang),
  )).toList(),
  onChanged: (value) {
    setState(() => _selectedLanguage = value!);
  },
  validator: (value) => value == null ? 'يرجى اختيار لغة' : null,
)

PopupMenuButton

PopupMenuButton يعرض قائمة خيارات عند الضغط. يُستخدم عادة لقوائم التجاوز (قائمة النقاط الثلاث) في أشرطة التطبيق وعناصر القوائم.

مثال على PopupMenuButton

PopupMenuButton<String>(
  onSelected: (value) {
    switch (value) {
      case 'edit':
        debugPrint('تم اختيار التعديل');
        break;
      case 'delete':
        debugPrint('تم اختيار الحذف');
        break;
      case 'share':
        debugPrint('تم اختيار المشاركة');
        break;
    }
  },
  itemBuilder: (context) => [
    const PopupMenuItem(
      value: 'edit',
      child: ListTile(
        leading: Icon(Icons.edit),
        title: Text('تعديل'),
        contentPadding: EdgeInsets.zero,
      ),
    ),
    const PopupMenuItem(
      value: 'share',
      child: ListTile(
        leading: Icon(Icons.share),
        title: Text('مشاركة'),
        contentPadding: EdgeInsets.zero,
      ),
    ),
    const PopupMenuDivider(),
    const PopupMenuItem(
      value: 'delete',
      child: ListTile(
        leading: Icon(Icons.delete, color: Colors.red),
        title: Text('حذف', style: TextStyle(color: Colors.red)),
        contentPadding: EdgeInsets.zero,
      ),
    ),
  ],
  icon: const Icon(Icons.more_vert),
)

ButtonStyle — تنسيق متسق

ButtonStyle هو نظام التنسيق الموحد لجميع أزرار Material. كل نوع زر لديه دالة مصنع styleFrom() للتنسيق المريح، لكن يمكنك أيضاً إنشاء كائنات ButtonStyle مباشرة لمزيد من التحكم.

تعمق في ButtonStyle

// استخدام styleFrom (مريح)
ElevatedButton(
  onPressed: () {},
  style: ElevatedButton.styleFrom(
    backgroundColor: Colors.indigo,
    foregroundColor: Colors.white,
    disabledBackgroundColor: Colors.grey.shade300,
    disabledForegroundColor: Colors.grey.shade500,
    elevation: 4,
    shadowColor: Colors.indigo.withOpacity(0.4),
    padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
    minimumSize: const Size(120, 48),
    maximumSize: const Size(300, 56),
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(12),
    ),
    textStyle: const TextStyle(
      fontSize: 16,
      fontWeight: FontWeight.w600,
      letterSpacing: 0.5,
    ),
  ),
  child: const Text('زر منسق'),
)

// استخدام ButtonStyle مباشرة (تحكم كامل)
ElevatedButton(
  onPressed: () {},
  style: ButtonStyle(
    backgroundColor: WidgetStateProperty.resolveWith((states) {
      if (states.contains(WidgetState.pressed)) {
        return Colors.indigo.shade700;
      }
      if (states.contains(WidgetState.hovered)) {
        return Colors.indigo.shade600;
      }
      if (states.contains(WidgetState.disabled)) {
        return Colors.grey.shade300;
      }
      return Colors.indigo;
    }),
    foregroundColor: WidgetStateProperty.all(Colors.white),
    overlayColor: WidgetStateProperty.all(
      Colors.white.withOpacity(0.1),
    ),
    animationDuration: const Duration(milliseconds: 200),
  ),
  child: const Text('نمط ديناميكي'),
)
تحذير: في Flutter 3.22+، تم إعادة تسمية MaterialStateProperty إلى WidgetStateProperty وMaterialState إلى WidgetState. الأسماء القديمة لا تزال تعمل لكنها مهملة. استخدم دائماً الأسماء الجديدة في الكود الجديد.

الحالة المعطلة

الزر يكون معطلاً عندما تكون دوال onPressedonLongPress) بقيمة null. يطبق Flutter تلقائياً تنسيق التعطيل — شفافية مخفضة وبدون رش حبر.

أنماط الأزرار المعطلة

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

  @override
  State<FormSubmitButton> createState() => _FormSubmitButtonState();
}

class _FormSubmitButtonState extends State<FormSubmitButton> {
  bool _isValid = false;
  bool _isLoading = false;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        SwitchListTile(
          title: const Text('النموذج صالح'),
          value: _isValid,
          onChanged: (v) => setState(() => _isValid = v),
        ),
        const SizedBox(height: 16),
        SizedBox(
          width: double.infinity,
          height: 48,
          child: ElevatedButton(
            // null = معطل، دالة = مُفعّل
            onPressed: (_isValid && !_isLoading)
                ? () async {
                    setState(() => _isLoading = true);
                    await Future.delayed(const Duration(seconds: 2));
                    if (mounted) setState(() => _isLoading = false);
                  }
                : null,
            child: _isLoading
                ? const SizedBox(
                    width: 20, height: 20,
                    child: CircularProgressIndicator(
                      strokeWidth: 2, color: Colors.white,
                    ),
                  )
                : const Text('إرسال'),
          ),
        ),
      ],
    );
  }
}

onPressed مقابل onLongPress

معظم الأزرار تدعم كلاً من onPressed (نقر) وonLongPress (ضغط مطول). استخدم onLongPress للإجراءات الثانوية أو لعرض خيارات إضافية.

الضغط والضغط المطول

ElevatedButton(
  onPressed: () {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('تم النقر!')),
    );
  },
  onLongPress: () {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('تم اكتشاف ضغط مطول'),
        content: const Text('لقد ضغطت مطولاً على الزر!'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('حسناً'),
          ),
        ],
      ),
    );
  },
  child: const Text('انقر أو اضغط مطولاً'),
)

مثال عملي: شريط الإجراءات

نمط واجهة مستخدم شائع هو شريط إجراءات مع أنواع أزرار متعددة:

ودجت شريط الإجراءات

class ActionBar extends StatelessWidget {
  final VoidCallback? onSave;
  final VoidCallback? onCancel;
  final VoidCallback? onDelete;
  final bool isLoading;

  const ActionBar({
    super.key,
    this.onSave,
    this.onCancel,
    this.onDelete,
    this.isLoading = false,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: Colors.white,
        boxShadow: [
          BoxShadow(
            color: Colors.black.withOpacity(0.05),
            blurRadius: 10,
            offset: const Offset(0, -2),
          ),
        ],
      ),
      child: Row(
        children: [
          // زر الحذف (تدميري)
          if (onDelete != null)
            OutlinedButton.icon(
              onPressed: isLoading ? null : onDelete,
              icon: const Icon(Icons.delete_outline),
              label: const Text('حذف'),
              style: OutlinedButton.styleFrom(
                foregroundColor: Colors.red,
                side: const BorderSide(color: Colors.red),
              ),
            ),
          const Spacer(),
          // زر الإلغاء
          TextButton(
            onPressed: isLoading ? null : onCancel,
            child: const Text('إلغاء'),
          ),
          const SizedBox(width: 12),
          // زر الحفظ (أساسي)
          ElevatedButton.icon(
            onPressed: isLoading ? null : onSave,
            icon: isLoading
                ? const SizedBox(
                    width: 16, height: 16,
                    child: CircularProgressIndicator(strokeWidth: 2),
                  )
                : const Icon(Icons.check),
            label: Text(isLoading ? 'جاري الحفظ...' : 'حفظ'),
          ),
        ],
      ),
    );
  }
}

مثال عملي: مجموعة أزرار منسقة

بناء مجموعة كاملة من الأزرار ذات السمة لتصميم تطبيق متسق:

مجموعة أزرار ذات سمة

class AppButton extends StatelessWidget {
  final String label;
  final IconData? icon;
  final VoidCallback? onPressed;
  final AppButtonVariant variant;
  final bool isLoading;
  final bool isFullWidth;

  const AppButton({
    super.key,
    required this.label,
    this.icon,
    this.onPressed,
    this.variant = AppButtonVariant.primary,
    this.isLoading = false,
    this.isFullWidth = false,
  });

  @override
  Widget build(BuildContext context) {
    final button = switch (variant) {
      AppButtonVariant.primary => ElevatedButton(
        onPressed: isLoading ? null : onPressed,
        style: ElevatedButton.styleFrom(
          backgroundColor: Colors.blue,
          foregroundColor: Colors.white,
          padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(10),
          ),
        ),
        child: _buildChild(),
      ),
      AppButtonVariant.secondary => OutlinedButton(
        onPressed: isLoading ? null : onPressed,
        style: OutlinedButton.styleFrom(
          foregroundColor: Colors.blue,
          side: const BorderSide(color: Colors.blue),
          padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(10),
          ),
        ),
        child: _buildChild(),
      ),
      AppButtonVariant.text => TextButton(
        onPressed: isLoading ? null : onPressed,
        style: TextButton.styleFrom(
          padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
        ),
        child: _buildChild(),
      ),
      AppButtonVariant.danger => ElevatedButton(
        onPressed: isLoading ? null : onPressed,
        style: ElevatedButton.styleFrom(
          backgroundColor: Colors.red,
          foregroundColor: Colors.white,
          padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
          shape: RoundedRectangleBorder(
            borderRadius: BorderRadius.circular(10),
          ),
        ),
        child: _buildChild(),
      ),
    };

    return isFullWidth
        ? SizedBox(width: double.infinity, child: button)
        : button;
  }

  Widget _buildChild() {
    if (isLoading) {
      return const SizedBox(
        width: 20, height: 20,
        child: CircularProgressIndicator(strokeWidth: 2),
      );
    }
    if (icon != null) {
      return Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          Icon(icon, size: 18),
          const SizedBox(width: 8),
          Text(label),
        ],
      );
    }
    return Text(label);
  }
}

enum AppButtonVariant { primary, secondary, text, danger }

// الاستخدام
Column(
  children: [
    AppButton(label: 'حفظ التغييرات', icon: Icons.save, onPressed: () {}),
    const SizedBox(height: 8),
    AppButton(label: 'تصدير', variant: AppButtonVariant.secondary, onPressed: () {}),
    const SizedBox(height: 8),
    AppButton(label: 'تخطي', variant: AppButtonVariant.text, onPressed: () {}),
    const SizedBox(height: 8),
    AppButton(label: 'حذف', variant: AppButtonVariant.danger, icon: Icons.delete, onPressed: () {}),
    const SizedBox(height: 8),
    AppButton(label: 'عرض كامل', isFullWidth: true, onPressed: () {}),
  ],
)

الملخص

في هذا الدرس، تعلمت:

  • ElevatedButton للإجراءات الأساسية عالية التأكيد مع ارتفاع وتعبئة
  • TextButton للإجراءات منخفضة التأكيد في مربعات الحوار والبطاقات وأشرطة الأدوات
  • OutlinedButton للإجراءات الثانوية متوسطة التأكيد مع حدود
  • IconButton مع متغيرات Material 3: مملوء ونغمي ومحدد
  • FloatingActionButton للإجراء الأساسي للشاشة (صغير وعادي وكبير وممتد)
  • DropdownButton للاختيار من قائمة؛ PopupMenuButton لقوائم التجاوز
  • ButtonStyle وstyleFrom() لتنسيق أزرار متسق وواعي للحالة
  • الأزرار تكون معطلة عندما يكون onPressed بقيمة null؛ استخدم onLongPress للتفاعلات الثانوية
ما التالي: في الدروس القادمة، سنستكشف ودجت التخطيط مثل Container وRow وColumn وStack والمزيد لنتعلم كيفية ترتيب وتموضع الودجت على الشاشة.