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

مربع الحوار، شريط الإشعار والورقة السفلية

50 دقيقة الدرس 11 من 18

مربعات الحوار وأشرطة الإشعار والأوراق السفلية

يوفر Flutter عدة ودجات مدمجة لعرض الرسائل المؤقتة وطلبات التأكيد ولوحات الإجراءات. في هذا الدرس ستتعلم كيفية استخدام AlertDialog و SimpleDialog و SnackBar و BottomSheet للتواصل مع المستخدمين بفعالية. هذه الودجات ضرورية لكل تطبيق إنتاجي لأنها تعطي المستخدمين تغذية راجعة وتطلب التأكيد وتعرض إجراءات سياقية.

ملاحظة: جميع دوال الحوار والأوراق في Flutter غير متزامنة. تعيد Future يُحل عند إغلاق الطبقة العلوية. هذا يعني أنه يمكنك استخدام await للنتيجة لمعرفة أي إجراء اختاره المستخدم.

showDialog و AlertDialog

دالة showDialog تعرض مربع حوار نمطي فوق المحتوى الحالي. أكثر ودجات الحوار شيوعاً هي AlertDialog التي توفر عنواناً ومنطقة محتوى وأزرار إجراءات.

AlertDialog أساسي

void _showBasicAlert(BuildContext context) {
  showDialog(
    context: context,
    builder: (BuildContext ctx) {
      return AlertDialog(
        title: const Text('حذف العنصر'),
        content: const Text(
          'هل أنت متأكد من حذف هذا العنصر؟ '
          'لا يمكن التراجع عن هذا الإجراء.',
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(ctx).pop(false),
            child: const Text('إلغاء'),
          ),
          TextButton(
            onPressed: () => Navigator.of(ctx).pop(true),
            child: const Text('حذف'),
          ),
        ],
      );
    },
  );
}

معالجة نتيجة مربع الحوار

بما أن showDialog تعيد Future يمكنك انتظار النتيجة الممررة إلى Navigator.pop.

انتظار نتيجة الحوار

Future<void> _confirmDelete(BuildContext context) async {
  final bool? confirmed = await showDialog<bool>(
    context: context,
    barrierDismissible: false, // يجب على المستخدم الضغط على زر
    builder: (BuildContext ctx) {
      return AlertDialog(
        title: const Text('تأكيد الحذف'),
        content: const Text('سيؤدي هذا إلى إزالة الملف نهائياً.'),
        actions: [
          TextButton(
            onPressed: () => Navigator.of(ctx).pop(false),
            child: const Text('إلغاء'),
          ),
          FilledButton(
            onPressed: () => Navigator.of(ctx).pop(true),
            style: FilledButton.styleFrom(
              backgroundColor: Colors.red,
            ),
            child: const Text('حذف'),
          ),
        ],
      );
    },
  );

  if (confirmed == true) {
    // تنفيذ الحذف
    debugPrint('تم حذف العنصر');
  }
}
نصيحة: عيّن barrierDismissible: false عندما يجب على المستخدم اختيار إجراء صريح. بشكل افتراضي يُغلق مربع الحوار عند الضغط خارجه مما قد يؤدي إلى نتائج غامضة.

تخصيص مظهر AlertDialog

يمكنك تخصيص الشكل ولون الخلفية والحشو والارتفاع لـ AlertDialog.

AlertDialog مُنسق

AlertDialog(
  shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(16),
  ),
  backgroundColor: Colors.grey.shade50,
  elevation: 8,
  titlePadding: const EdgeInsets.fromLTRB(24, 24, 24, 0),
  contentPadding: const EdgeInsets.fromLTRB(24, 16, 24, 0),
  actionsPadding: const EdgeInsets.all(16),
  title: const Text('حوار مخصص'),
  content: const Text('هذا الحوار له تنسيق مخصص.'),
  actions: [
    TextButton(
      onPressed: () => Navigator.pop(context),
      child: const Text('حسناً'),
    ),
  ],
)

SimpleDialog

عندما تحتاج أن يختار المستخدم من قائمة خيارات بدلاً من التأكيد أو الإلغاء استخدم SimpleDialog. كل خيار هو SimpleDialogOption.

مثال SimpleDialog

Future<void> _chooseLanguage(BuildContext context) async {
  final String? selected = await showDialog<String>(
    context: context,
    builder: (BuildContext ctx) {
      return SimpleDialog(
        title: const Text('اختر اللغة'),
        children: [
          SimpleDialogOption(
            onPressed: () => Navigator.pop(ctx, 'en'),
            child: const Text('الإنجليزية'),
          ),
          SimpleDialogOption(
            onPressed: () => Navigator.pop(ctx, 'ar'),
            child: const Text('العربية'),
          ),
          SimpleDialogOption(
            onPressed: () => Navigator.pop(ctx, 'es'),
            child: const Text('الإسبانية'),
          ),
        ],
      );
    },
  );

  if (selected != null) {
    debugPrint('المستخدم اختار: \$selected');
  }
}

showSnackBar و SnackBar

SnackBar هو شريط رسائل خفيف يظهر في أسفل الشاشة. إنه مثالي لرسائل التغذية الراجعة القصيرة مثل "تم حفظ العنصر" أو "انقطع الاتصال". تعرض SnackBar من خلال ScaffoldMessenger.

SnackBar أساسي

void _showSnackBar(BuildContext context) {
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(
      content: Text('تم حفظ العنصر بنجاح!'),
    ),
  );
}

SnackBar مع إجراء

يمكن أن تتضمن SnackBars زر إجراء يُستخدم عادة لعمليات التراجع.

SnackBar مع إجراء تراجع

void _deleteWithUndo(BuildContext context, String itemName) {
  ScaffoldMessenger.of(context).showSnackBar(
    SnackBar(
      content: Text('تم حذف \$itemName'),
      duration: const Duration(seconds: 5),
      action: SnackBarAction(
        label: 'تراجع',
        onPressed: () {
          // استعادة العنصر المحذوف
          debugPrint('التراجع عن حذف \$itemName');
        },
      ),
    ),
  );
}

سلوك وتنسيق SnackBar

يمكنك التحكم في مكان ظهور SnackBar وكيف يبدو.

SnackBar عائم

ScaffoldMessenger.of(context).showSnackBar(
  SnackBar(
    content: const Text('هذا شريط إشعار عائم'),
    behavior: SnackBarBehavior.floating,
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(12),
    ),
    margin: const EdgeInsets.all(16),
    backgroundColor: Colors.green.shade700,
    duration: const Duration(seconds: 3),
  ),
);
ملاحظة: استخدم ScaffoldMessenger.of(context) بدلاً من Scaffold.of(context).showSnackBar المهمل. نهج ScaffoldMessenger يعمل حتى عند إعادة بناء Scaffold ويستمر عبر تغييرات المسارات.

showModalBottomSheet

الورقة السفلية النمطية تنزلق من أسفل الشاشة وتمنع التفاعل مع المحتوى خلفها. إنها مثالية لتقديم مجموعة إجراءات أو نموذج قصير.

ورقة سفلية نمطية أساسية

void _showActions(BuildContext context) {
  showModalBottomSheet(
    context: context,
    shape: const RoundedRectangleBorder(
      borderRadius: BorderRadius.vertical(
        top: Radius.circular(20),
      ),
    ),
    builder: (BuildContext ctx) {
      return Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            ListTile(
              leading: const Icon(Icons.camera_alt),
              title: const Text('التقاط صورة'),
              onTap: () {
                Navigator.pop(ctx);
                debugPrint('تم اختيار الكاميرا');
              },
            ),
            ListTile(
              leading: const Icon(Icons.photo_library),
              title: const Text('اختيار من المعرض'),
              onTap: () {
                Navigator.pop(ctx);
                debugPrint('تم اختيار المعرض');
              },
            ),
            ListTile(
              leading: const Icon(Icons.delete),
              title: const Text('إزالة الصورة'),
              onTap: () {
                Navigator.pop(ctx);
                debugPrint('تم اختيار الإزالة');
              },
            ),
          ],
        ),
      );
    },
  );
}

showBottomSheet (غير نمطي)

على عكس النسخة النمطية لا يمنع showBottomSheet التفاعل مع بقية الشاشة. يُستدعى من سياق Scaffold.

ورقة سفلية مستمرة

void _showPersistentSheet(BuildContext context) {
  Scaffold.of(context).showBottomSheet(
    (BuildContext ctx) {
      return Container(
        padding: const EdgeInsets.all(16),
        decoration: BoxDecoration(
          color: Colors.blue.shade50,
          borderRadius: const BorderRadius.vertical(
            top: Radius.circular(16),
          ),
        ),
        child: const Text(
          'هذه ورقة سفلية مستمرة. '
          'يمكنك التفاعل مع المحتوى خلفها.',
        ),
      );
    },
  );
}

DraggableScrollableSheet

للأوراق السفلية التي يمكن للمستخدم سحبها لتوسيعها أو طيها استخدم DraggableScrollableSheet داخل ورقة سفلية نمطية.

ورقة سفلية قابلة للسحب

void _showDraggableSheet(BuildContext context) {
  showModalBottomSheet(
    context: context,
    isScrollControlled: true,
    builder: (BuildContext ctx) {
      return DraggableScrollableSheet(
        initialChildSize: 0.4,
        minChildSize: 0.2,
        maxChildSize: 0.9,
        expand: false,
        builder: (BuildContext context, ScrollController controller) {
          return Container(
            decoration: const BoxDecoration(
              color: Colors.white,
              borderRadius: BorderRadius.vertical(
                top: Radius.circular(20),
              ),
            ),
            child: ListView.builder(
              controller: controller,
              itemCount: 30,
              itemBuilder: (context, index) {
                return ListTile(
                  title: Text('عنصر \${index + 1}'),
                );
              },
            ),
          );
        },
      );
    },
  );
}
نصيحة: عيّن isScrollControlled: true على الورقة السفلية النمطية عند استخدام DraggableScrollableSheet. بدونها يكون ارتفاع الورقة محدوداً بنصف الشاشة.

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

الجمع بين AlertDialog و SnackBar يعطي تجربة مستخدم كاملة للإجراءات التدميرية.

تدفق الحذف الكامل

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

  @override
  State<ItemListScreen> createState() => _ItemListScreenState();
}

class _ItemListScreenState extends State<ItemListScreen> {
  final List<String> _items = [
    'المستند أ',
    'المستند ب',
    'المستند ج',
  ];

  Future<void> _handleDelete(int index) async {
    final confirmed = await showDialog<bool>(
      context: context,
      builder: (ctx) => AlertDialog(
        title: const Text('حذف المستند'),
        content: Text(
          'حذف "\${_items[index]}"؟ لا يمكن التراجع عن هذا.',
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(ctx, false),
            child: const Text('إلغاء'),
          ),
          FilledButton(
            onPressed: () => Navigator.pop(ctx, true),
            style: FilledButton.styleFrom(
              backgroundColor: Colors.red,
            ),
            child: const Text('حذف'),
          ),
        ],
      ),
    );

    if (confirmed == true) {
      final removedItem = _items[index];
      setState(() => _items.removeAt(index));

      if (mounted) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(
            content: Text('تم حذف \$removedItem'),
            action: SnackBarAction(
              label: 'تراجع',
              onPressed: () {
                setState(() => _items.insert(index, removedItem));
              },
            ),
          ),
        );
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('مستنداتي')),
      body: ListView.builder(
        itemCount: _items.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(_items[index]),
            trailing: IconButton(
              icon: const Icon(Icons.delete),
              onPressed: () => _handleDelete(index),
            ),
          );
        },
      ),
    );
  }
}

مثال عملي: ورقة الإجراءات

ورقة سفلية تعرض إجراءات مثل المشاركة والتعديل أو حذف مورد.

ورقة إجراءات مع أيقونات

void _showActionSheet(BuildContext context) {
  showModalBottomSheet(
    context: context,
    shape: const RoundedRectangleBorder(
      borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
    ),
    builder: (ctx) {
      return SafeArea(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Container(
              width: 40,
              height: 4,
              margin: const EdgeInsets.symmetric(vertical: 12),
              decoration: BoxDecoration(
                color: Colors.grey.shade300,
                borderRadius: BorderRadius.circular(2),
              ),
            ),
            ListTile(
              leading: const Icon(Icons.share),
              title: const Text('مشاركة'),
              onTap: () => Navigator.pop(ctx),
            ),
            ListTile(
              leading: const Icon(Icons.edit),
              title: const Text('تعديل'),
              onTap: () => Navigator.pop(ctx),
            ),
            ListTile(
              leading: const Icon(Icons.bookmark_add),
              title: const Text('إشارة مرجعية'),
              onTap: () => Navigator.pop(ctx),
            ),
            const Divider(),
            ListTile(
              leading: const Icon(Icons.delete, color: Colors.red),
              title: const Text(
                'حذف',
                style: TextStyle(color: Colors.red),
              ),
              onTap: () => Navigator.pop(ctx),
            ),
            const SizedBox(height: 8),
          ],
        ),
      );
    },
  );
}

ملخص

  • showDialog + AlertDialog -- حوارات التأكيد أو المعلومات النمطية
  • SimpleDialog -- الاختيار من قائمة خيارات
  • ScaffoldMessenger.showSnackBar -- رسائل تغذية راجعة قصيرة في الأسفل
  • showModalBottomSheet -- أوراق إجراءات ونماذج قصيرة تمنع التفاعل مع الخلفية
  • showBottomSheet -- أوراق مستمرة تسمح بالتفاعل مع الخلفية
  • DraggableScrollableSheet -- أوراق قابلة للتوسيع يمكن للمستخدم تغيير حجمها بالسحب
  • جميع دوال الطبقات العلوية تعيد قيم Future يمكنك انتظارها باستخدام await

تمرين عملي

ابنِ شاشة تطبيق ملاحظات مع قائمة ملاحظات. أضف زر إجراء عائم يفتح ورقة سفلية مع حقل نص لإضافة ملاحظة جديدة. كل عنصر ملاحظة يجب أن يحتوي على أيقونة حذف تعرض AlertDialog للتأكيد. بعد الحذف اعرض SnackBar مع إجراء تراجع يستعيد الملاحظة.