تخطيطات Flutter والتصميم المتجاوب

بناء تطبيق متجاوب بالكامل

60 دقيقة الدرس 16 من 16

مشروع ختامي: تطبيق لوحة تحكم متجاوبة

في هذا الدرس الختامي، نجمع كل مفاهيم التخطيط من هذا البرنامج التعليمي لبناء تطبيق لوحة تحكم متجاوب بالكامل. تتكيف لوحة التحكم هذه بسلاسة عبر الهواتف والأجهزة اللوحية وأجهزة سطح المكتب مع تنقل تكيفي وشبكات متجاوبة وشريط تطبيق قابل للطي ونمط رئيسي-تفصيلي ومعالجة مناسبة للمناطق الآمنة.

ما سنبنيه: لوحة تحكم تحليلات كاملة مع: (1) تنقل تكيفي يتبدل بين التنقل السفلي وقضيب التنقل والدرج الكامل، (2) شبكة متجاوبة تضبط الأعمدة بناءً على عرض الشاشة، (3) شريط تطبيق قابل للطي مع بحث، (4) نمط رئيسي-تفصيلي لاستكشاف البيانات، و (5) معالجة المناطق الآمنة في كل مكان.

الخطوة 1: أدوات الاستجابة

أولاً، لننشئ الأدوات الأساسية التي سيستخدمها التطبيق بأكمله — نقاط التوقف واكتشاف نوع الجهاز وعنصر البناء المتجاوب.

responsive_utils.dart

import 'package:flutter/material.dart';

enum DeviceType { phone, tablet, desktop }

class Breakpoints {
  static const double phone = 0;
  static const double tablet = 600;
  static const double desktop = 1024;
  static const double desktopLarge = 1440;

  static DeviceType getDeviceType(double width) {
    if (width >= desktop) return DeviceType.desktop;
    if (width >= tablet) return DeviceType.tablet;
    return DeviceType.phone;
  }

  static int getGridColumns(double width) {
    if (width >= desktopLarge) return 4;
    if (width >= desktop) return 3;
    if (width >= tablet) return 2;
    return 1;
  }

  static double getContentMaxWidth(double width) {
    if (width >= desktopLarge) return 1200;
    if (width >= desktop) return 960;
    return double.infinity;
  }
}

class ResponsiveBuilder extends StatelessWidget {
  final Widget Function(BuildContext, DeviceType, double) builder;

  const ResponsiveBuilder({super.key, required this.builder});

  @override
  Widget build(BuildContext context) {
    final width = MediaQuery.sizeOf(context).width;
    final deviceType = Breakpoints.getDeviceType(width);
    return builder(context, deviceType, width);
  }
}

class ResponsiveConstrainedBox extends StatelessWidget {
  final Widget child;

  const ResponsiveConstrainedBox({
    super.key,
    required this.child,
  });

  @override
  Widget build(BuildContext context) {
    final width = MediaQuery.sizeOf(context).width;
    final maxWidth = Breakpoints.getContentMaxWidth(width);

    return Center(
      child: ConstrainedBox(
        constraints: BoxConstraints(maxWidth: maxWidth),
        child: child,
      ),
    );
  }
}

الخطوة 2: سمة التطبيق والثوابت

تحديد سمة متسقة ونماذج بيانات مستخدمة في جميع أنحاء لوحة التحكم.

dashboard_theme.dart

import 'package:flutter/material.dart';

class DashboardTheme {
  static ThemeData get lightTheme => ThemeData(
    useMaterial3: true,
    colorSchemeSeed: Colors.indigo,
    brightness: Brightness.light,
    cardTheme: CardTheme(
      elevation: 0,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(12),
        side: BorderSide(color: Colors.grey.shade200),
      ),
    ),
    appBarTheme: const AppBarTheme(
      centerTitle: false,
      elevation: 0,
    ),
  );
}

class DashboardItem {
  final String title;
  final String value;
  final IconData icon;
  final Color color;
  final double change;

  const DashboardItem({
    required this.title,
    required this.value,
    required this.icon,
    required this.color,
    required this.change,
  });
}

class ActivityItem {
  final String title;
  final String description;
  final String time;
  final IconData icon;

  const ActivityItem({
    required this.title,
    required this.description,
    required this.time,
    required this.icon,
  });
}

// بيانات نموذجية
const List<DashboardItem> kDashboardStats = [
  DashboardItem(
    title: 'إجمالي المستخدمين',
    value: '24,563',
    icon: Icons.people,
    color: Colors.blue,
    change: 12.5,
  ),
  DashboardItem(
    title: 'الإيرادات',
    value: '\$48,290',
    icon: Icons.attach_money,
    color: Colors.green,
    change: 8.3,
  ),
  DashboardItem(
    title: 'الطلبات',
    value: '1,847',
    icon: Icons.shopping_cart,
    color: Colors.orange,
    change: -2.1,
  ),
  DashboardItem(
    title: 'معدل التحويل',
    value: '3.24%',
    icon: Icons.trending_up,
    color: Colors.purple,
    change: 5.7,
  ),
];

const List<ActivityItem> kRecentActivity = [
  ActivityItem(
    title: 'مستخدم جديد مسجل',
    description: 'أنشأ جون دو حسابًا',
    time: 'منذ 2 دقيقة',
    icon: Icons.person_add,
  ),
  ActivityItem(
    title: 'طلب مكتمل',
    description: 'تم تسليم الطلب #1234',
    time: 'منذ 15 دقيقة',
    icon: Icons.check_circle,
  ),
  ActivityItem(
    title: 'دفعة مستلمة',
    description: '\$250.00 من جين سميث',
    time: 'منذ ساعة',
    icon: Icons.payment,
  ),
  ActivityItem(
    title: 'مراجعة جديدة',
    description: 'مراجعة 5 نجوم على المنتج أ',
    time: 'منذ ساعتين',
    icon: Icons.star,
  ),
  ActivityItem(
    title: 'تنبيه المخزون',
    description: 'المنتج ب ينفد',
    time: 'منذ 3 ساعات',
    icon: Icons.warning,
  ),
  ActivityItem(
    title: 'حملة أُطلقت',
    description: 'حملة التخفيضات الصيفية مباشرة',
    time: 'منذ 5 ساعات',
    icon: Icons.campaign,
  ),
];

الخطوة 3: غلاف التنقل التكيفي

غلاف التنقل هو قلب تطبيقنا المتجاوب. يتبدل بين شريط التنقل السفلي على الهواتف وقضيب التنقل المدمج على الأجهزة اللوحية وقضيب التنقل الموسع الكامل (شبيه بالدرج) على أجهزة سطح المكتب.

adaptive_shell.dart

import 'package:flutter/material.dart';

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

  @override
  State<AdaptiveShell> createState() => _AdaptiveShellState();
}

class _AdaptiveShellState extends State<AdaptiveShell> {
  int _selectedIndex = 0;

  static const _navItems = [
    (icon: Icons.dashboard, label: 'لوحة التحكم'),
    (icon: Icons.analytics, label: 'التحليلات'),
    (icon: Icons.people, label: 'المستخدمون'),
    (icon: Icons.settings, label: 'الإعدادات'),
  ];

  @override
  Widget build(BuildContext context) {
    return ResponsiveBuilder(
      builder: (context, deviceType, width) {
        return Scaffold(
          body: Row(
            children: [
              // سطح المكتب: قضيب تنقل موسع (مثل الدرج)
              if (deviceType == DeviceType.desktop)
                NavigationRail(
                  extended: width >= Breakpoints.desktopLarge,
                  minExtendedWidth: 220,
                  selectedIndex: _selectedIndex,
                  onDestinationSelected: _onItemSelected,
                  leading: Padding(
                    padding: const EdgeInsets.symmetric(vertical: 16),
                    child: width >= Breakpoints.desktopLarge
                        ? const Row(
                            mainAxisSize: MainAxisSize.min,
                            children: [
                              Icon(Icons.dashboard_customize,
                                  color: Colors.indigo),
                              SizedBox(width: 8),
                              Text(
                                'لوحة التحكم',
                                style: TextStyle(
                                  fontSize: 18,
                                  fontWeight: FontWeight.bold,
                                ),
                              ),
                            ],
                          )
                        : const Icon(Icons.dashboard_customize,
                            color: Colors.indigo, size: 32),
                  ),
                  destinations: _navItems
                      .map((item) => NavigationRailDestination(
                            icon: Icon(item.icon),
                            label: Text(item.label),
                          ))
                      .toList(),
                )

              // الجهاز اللوحي: قضيب تنقل مدمج
              else if (deviceType == DeviceType.tablet)
                NavigationRail(
                  extended: false,
                  selectedIndex: _selectedIndex,
                  onDestinationSelected: _onItemSelected,
                  leading: const Padding(
                    padding: EdgeInsets.symmetric(vertical: 8),
                    child: Icon(Icons.dashboard_customize,
                        color: Colors.indigo, size: 28),
                  ),
                  destinations: _navItems
                      .map((item) => NavigationRailDestination(
                            icon: Icon(item.icon),
                            label: Text(item.label),
                          ))
                      .toList(),
                ),

              // محتوى الصفحة
              Expanded(
                child: _buildPage(_selectedIndex),
              ),
            ],
          ),

          // الهاتف: شريط التنقل السفلي
          bottomNavigationBar: deviceType == DeviceType.phone
              ? NavigationBar(
                  selectedIndex: _selectedIndex,
                  onDestinationSelected: _onItemSelected,
                  destinations: _navItems
                      .map((item) => NavigationDestination(
                            icon: Icon(item.icon),
                            label: item.label,
                          ))
                      .toList(),
                )
              : null,
        );
      },
    );
  }

  void _onItemSelected(int index) {
    setState(() => _selectedIndex = index);
  }

  Widget _buildPage(int index) {
    return switch (index) {
      0 => const DashboardPage(),
      1 => const AnalyticsPage(),
      2 => const UsersPage(),
      3 => const SettingsPage(),
      _ => const DashboardPage(),
    };
  }
}
نصيحة: على أجهزة سطح المكتب الكبيرة جدًا (1440 بكسل+)، يتوسع قضيب التنقل لإظهار التسميات. على أجهزة سطح المكتب القياسية، يبقى مدمجًا مع الأيقونات فقط. هذا التحسين التدريجي يضمن أن التنقل لا يهدر المساحة أبدًا.

الخطوة 4: صفحة لوحة التحكم مع شريط التطبيق القابل للطي

تتميز صفحة لوحة التحكم الرئيسية بـ SliverAppBar مع وظيفة البحث التي تنطوي أثناء التمرير، تليها بطاقات إحصائيات متجاوبة وموجز النشاط.

dashboard_page.dart

class DashboardPage extends StatelessWidget {
  const DashboardPage({super.key});

  @override
  Widget build(BuildContext context) {
    return ResponsiveBuilder(
      builder: (context, deviceType, width) {
        return CustomScrollView(
          slivers: [
            // شريط تطبيق قابل للطي مع بحث
            SliverAppBar(
              expandedHeight: deviceType == DeviceType.phone
                  ? 140
                  : 160,
              floating: true,
              pinned: true,
              snap: true,
              flexibleSpace: FlexibleSpaceBar(
                titlePadding: const EdgeInsets.only(
                  left: 16,
                  bottom: 16,
                  right: 16,
                ),
                title: deviceType == DeviceType.phone
                    ? null
                    : const Text(
                        'نظرة عامة على لوحة التحكم',
                        style: TextStyle(fontSize: 16),
                      ),
                background: SafeArea(
                  bottom: false,
                  child: Padding(
                    padding: const EdgeInsets.fromLTRB(16, 16, 16, 48),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        if (deviceType == DeviceType.phone)
                          const Text(
                            'لوحة التحكم',
                            style: TextStyle(
                              fontSize: 24,
                              fontWeight: FontWeight.bold,
                            ),
                          ),
                        const Spacer(),
                        // شريط البحث
                        TextField(
                          decoration: InputDecoration(
                            hintText: 'البحث في لوحة التحكم...',
                            prefixIcon: const Icon(Icons.search),
                            filled: true,
                            fillColor: Theme.of(context)
                                .colorScheme
                                .surfaceContainerHighest
                                .withValues(alpha: 0.5),
                            border: OutlineInputBorder(
                              borderRadius: BorderRadius.circular(12),
                              borderSide: BorderSide.none,
                            ),
                            contentPadding:
                                const EdgeInsets.symmetric(vertical: 12),
                          ),
                        ),
                      ],
                    ),
                  ),
                ),
              ),
            ),

            // شبكة الإحصائيات
            SliverPadding(
              padding: const EdgeInsets.all(16),
              sliver: SliverGrid(
                gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                  crossAxisCount: Breakpoints.getGridColumns(width),
                  mainAxisSpacing: 12,
                  crossAxisSpacing: 12,
                  childAspectRatio: deviceType == DeviceType.phone
                      ? 2.5
                      : 2.0,
                ),
                delegate: SliverChildBuilderDelegate(
                  (context, index) =>
                      StatCard(item: kDashboardStats[index]),
                  childCount: kDashboardStats.length,
                ),
              ),
            ),

            // عنوان القسم
            const SliverToBoxAdapter(
              child: Padding(
                padding: EdgeInsets.fromLTRB(16, 8, 16, 8),
                child: Text(
                  'النشاط الأخير',
                  style: TextStyle(
                    fontSize: 20,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ),
            ),

            // موجز النشاط
            SliverPadding(
              padding: const EdgeInsets.symmetric(horizontal: 16),
              sliver: SliverList.separated(
                itemCount: kRecentActivity.length,
                itemBuilder: (context, index) =>
                    ActivityCard(item: kRecentActivity[index]),
                separatorBuilder: (_, __) => const SizedBox(height: 8),
              ),
            ),

            // مسافة المنطقة الآمنة السفلية
            SliverToBoxAdapter(
              child: SizedBox(
                height: MediaQuery.paddingOf(context).bottom + 16,
              ),
            ),
          ],
        );
      },
    );
  }
}

الخطوة 5: عناصر بطاقة الإحصائيات وبطاقة النشاط

عناصر البطاقات القابلة لإعادة الاستخدام هذه تكيف تخطيطها بناءً على المساحة المتاحة.

stat_card.dart

class StatCard extends StatelessWidget {
  final DashboardItem item;

  const StatCard({super.key, required this.item});

  @override
  Widget build(BuildContext context) {
    final isPositive = item.change >= 0;

    return Card(
      child: Padding(
        padding: const EdgeInsets.all(16),
        child: Row(
          children: [
            Container(
              padding: const EdgeInsets.all(12),
              decoration: BoxDecoration(
                color: item.color.withValues(alpha: 0.1),
                borderRadius: BorderRadius.circular(12),
              ),
              child: Icon(item.icon, color: item.color, size: 24),
            ),
            const SizedBox(width: 16),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Text(
                    item.title,
                    style: TextStyle(
                      color: Colors.grey[600],
                      fontSize: 13,
                    ),
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                  ),
                  const SizedBox(height: 4),
                  Text(
                    item.value,
                    style: const TextStyle(
                      fontSize: 22,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ],
              ),
            ),
            Container(
              padding: const EdgeInsets.symmetric(
                horizontal: 8,
                vertical: 4,
              ),
              decoration: BoxDecoration(
                color: (isPositive ? Colors.green : Colors.red)
                    .withValues(alpha: 0.1),
                borderRadius: BorderRadius.circular(8),
              ),
              child: Row(
                mainAxisSize: MainAxisSize.min,
                children: [
                  Icon(
                    isPositive
                        ? Icons.arrow_upward
                        : Icons.arrow_downward,
                    size: 14,
                    color: isPositive ? Colors.green : Colors.red,
                  ),
                  Text(
                    '\${item.change.abs()}%',
                    style: TextStyle(
                      color: isPositive ? Colors.green : Colors.red,
                      fontWeight: FontWeight.bold,
                      fontSize: 12,
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class ActivityCard extends StatelessWidget {
  final ActivityItem item;

  const ActivityCard({super.key, required this.item});

  @override
  Widget build(BuildContext context) {
    return Card(
      child: ListTile(
        leading: CircleAvatar(
          backgroundColor:
              Theme.of(context).colorScheme.primaryContainer,
          child: Icon(
            item.icon,
            color: Theme.of(context).colorScheme.primary,
            size: 20,
          ),
        ),
        title: Text(
          item.title,
          style: const TextStyle(fontWeight: FontWeight.w600),
        ),
        subtitle: Text(item.description),
        trailing: Text(
          item.time,
          style: TextStyle(
            color: Colors.grey[500],
            fontSize: 12,
          ),
        ),
      ),
    );
  }
}

الخطوة 6: صفحة المستخدمين مع نمط رئيسي-تفصيلي

تُظهر صفحة المستخدمين نمط رئيسي-تفصيلي. على الهواتف، اختيار مستخدم ينتقل إلى صفحة التفاصيل. على الأجهزة اللوحية وأجهزة سطح المكتب، تظهر لوحة التفاصيل بجانب القائمة.

users_page.dart

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

  @override
  State<UsersPage> createState() => _UsersPageState();
}

class _UsersPageState extends State<UsersPage> {
  int? _selectedUserIndex;

  final List<Map<String, dynamic>> _users = List.generate(
    15,
    (i) => {
      'name': 'مستخدم \${i + 1}',
      'email': 'user\${i + 1}@example.com',
      'role': i % 3 == 0 ? 'مدير' : (i % 3 == 1 ? 'محرر' : 'مشاهد'),
      'status': i % 4 != 0 ? 'نشط' : 'غير نشط',
      'joined': 'منذ \${12 - (i % 12)} شهر',
      'orders': (i + 1) * 7,
      'revenue': (i + 1) * 142.50,
    },
  );

  @override
  Widget build(BuildContext context) {
    return ResponsiveBuilder(
      builder: (context, deviceType, width) {
        final showDetail = deviceType != DeviceType.phone;

        return SafeArea(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              // الرأس
              Padding(
                padding: const EdgeInsets.all(16),
                child: Row(
                  children: [
                    const Expanded(
                      child: Text(
                        'المستخدمون',
                        style: TextStyle(
                          fontSize: 24,
                          fontWeight: FontWeight.bold,
                        ),
                      ),
                    ),
                    FilledButton.icon(
                      onPressed: () {},
                      icon: const Icon(Icons.add, size: 18),
                      label: Text(
                        deviceType == DeviceType.phone
                            ? 'إضافة'
                            : 'إضافة مستخدم',
                      ),
                    ),
                  ],
                ),
              ),

              // المحتوى
              Expanded(
                child: showDetail
                    ? Row(
                        children: [
                          SizedBox(
                            width: deviceType == DeviceType.desktop
                                ? 400
                                : 320,
                            child: _buildUserList(),
                          ),
                          const VerticalDivider(width: 1),
                          Expanded(
                            child: _selectedUserIndex != null
                                ? _buildUserDetail(
                                    _users[_selectedUserIndex!])
                                : const Center(
                                    child: Column(
                                      mainAxisSize: MainAxisSize.min,
                                      children: [
                                        Icon(
                                          Icons.person_search,
                                          size: 64,
                                          color: Colors.grey,
                                        ),
                                        SizedBox(height: 16),
                                        Text(
                                          'اختر مستخدمًا لعرض التفاصيل',
                                          style: TextStyle(
                                            color: Colors.grey,
                                            fontSize: 16,
                                          ),
                                        ),
                                      ],
                                    ),
                                  ),
                          ),
                        ],
                      )
                    : _buildUserList(),
              ),
            ],
          ),
        );
      },
    );
  }

  Widget _buildUserList() {
    return ListView.builder(
      itemCount: _users.length,
      itemBuilder: (context, index) {
        final user = _users[index];
        final isSelected = _selectedUserIndex == index;

        return ListTile(
          selected: isSelected,
          selectedTileColor: Colors.indigo.withValues(alpha: 0.08),
          leading: CircleAvatar(
            backgroundColor: Colors.primaries[index % Colors.primaries.length],
            child: Text(
              user['name'][0],
              style: const TextStyle(color: Colors.white),
            ),
          ),
          title: Text(
            user['name'],
            style: const TextStyle(fontWeight: FontWeight.w600),
          ),
          subtitle: Text(user['email']),
          trailing: Container(
            padding: const EdgeInsets.symmetric(
              horizontal: 8,
              vertical: 2,
            ),
            decoration: BoxDecoration(
              color: user['status'] == 'نشط'
                  ? Colors.green.withValues(alpha: 0.1)
                  : Colors.grey.withValues(alpha: 0.1),
              borderRadius: BorderRadius.circular(12),
            ),
            child: Text(
              user['status'],
              style: TextStyle(
                fontSize: 12,
                color: user['status'] == 'نشط'
                    ? Colors.green
                    : Colors.grey,
              ),
            ),
          ),
          onTap: () {
            setState(() => _selectedUserIndex = index);
            final width = MediaQuery.sizeOf(context).width;
            if (width < Breakpoints.tablet) {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (_) => Scaffold(
                    appBar: AppBar(title: Text(user['name'])),
                    body: _buildUserDetail(user),
                  ),
                ),
              );
            }
          },
        );
      },
    );
  }

  Widget _buildUserDetail(Map<String, dynamic> user) {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(24),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // رأس المستخدم
          Center(
            child: Column(
              children: [
                CircleAvatar(
                  radius: 40,
                  backgroundColor: Colors.indigo,
                  child: Text(
                    user['name'][0],
                    style: const TextStyle(
                      color: Colors.white,
                      fontSize: 32,
                    ),
                  ),
                ),
                const SizedBox(height: 12),
                Text(
                  user['name'],
                  style: const TextStyle(
                    fontSize: 22,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(height: 4),
                Text(
                  user['email'],
                  style: TextStyle(color: Colors.grey[600]),
                ),
              ],
            ),
          ),
          const SizedBox(height: 24),

          // بطاقات المعلومات
          _buildInfoRow('الدور', user['role']),
          _buildInfoRow('الحالة', user['status']),
          _buildInfoRow('تاريخ الانضمام', user['joined']),
          _buildInfoRow('إجمالي الطلبات', '\${user['orders']}'),
          _buildInfoRow(
            'إجمالي الإيرادات',
            '\$\${user['revenue'].toStringAsFixed(2)}',
          ),

          const SizedBox(height: 24),

          // أزرار الإجراءات
          Row(
            children: [
              Expanded(
                child: OutlinedButton.icon(
                  onPressed: () {},
                  icon: const Icon(Icons.edit),
                  label: const Text('تعديل'),
                ),
              ),
              const SizedBox(width: 12),
              Expanded(
                child: FilledButton.icon(
                  onPressed: () {},
                  icon: const Icon(Icons.email),
                  label: const Text('تواصل'),
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }

  Widget _buildInfoRow(String label, String value) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(
            label,
            style: TextStyle(
              color: Colors.grey[600],
              fontSize: 14,
            ),
          ),
          Text(
            value,
            style: const TextStyle(
              fontWeight: FontWeight.w600,
              fontSize: 14,
            ),
          ),
        ],
      ),
    );
  }
}

الخطوة 7: صفحات التحليلات والإعدادات

صفحات مبدئية تُظهر أيضًا مبادئ التخطيط المتجاوب.

analytics_page.dart و settings_page.dart

class AnalyticsPage extends StatelessWidget {
  const AnalyticsPage({super.key});

  @override
  Widget build(BuildContext context) {
    return ResponsiveBuilder(
      builder: (context, deviceType, width) {
        final columns = Breakpoints.getGridColumns(width);
        return SafeArea(
          child: CustomScrollView(
            slivers: [
              const SliverToBoxAdapter(
                child: Padding(
                  padding: EdgeInsets.all(16),
                  child: Text(
                    'التحليلات',
                    style: TextStyle(
                      fontSize: 24,
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                ),
              ),
              SliverPadding(
                padding: const EdgeInsets.symmetric(horizontal: 16),
                sliver: SliverGrid(
                  gridDelegate:
                      SliverGridDelegateWithFixedCrossAxisCount(
                    crossAxisCount: columns,
                    mainAxisSpacing: 12,
                    crossAxisSpacing: 12,
                  ),
                  delegate: SliverChildBuilderDelegate(
                    (context, index) {
                      return Card(
                        child: Padding(
                          padding: const EdgeInsets.all(16),
                          child: Column(
                            crossAxisAlignment:
                                CrossAxisAlignment.start,
                            children: [
                              Row(
                                children: [
                                  Icon(
                                    Icons.bar_chart,
                                    color: Colors.primaries[
                                        index %
                                            Colors.primaries.length],
                                  ),
                                  const SizedBox(width: 8),
                                  Text(
                                    'مقياس \${index + 1}',
                                    style: const TextStyle(
                                      fontWeight: FontWeight.bold,
                                    ),
                                  ),
                                ],
                              ),
                              const Spacer(),
                              // عنصر نائب للرسم البياني
                              Expanded(
                                flex: 2,
                                child: Container(
                                  decoration: BoxDecoration(
                                    color: Colors.grey.shade100,
                                    borderRadius:
                                        BorderRadius.circular(8),
                                  ),
                                  child: const Center(
                                    child: Icon(
                                      Icons.show_chart,
                                      size: 32,
                                      color: Colors.grey,
                                    ),
                                  ),
                                ),
                              ),
                            ],
                          ),
                        ),
                      );
                    },
                    childCount: 8,
                  ),
                ),
              ),
              SliverToBoxAdapter(
                child: SizedBox(
                  height: MediaQuery.paddingOf(context).bottom + 16,
                ),
              ),
            ],
          ),
        );
      },
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: ResponsiveConstrainedBox(
        child: ListView(
          padding: const EdgeInsets.all(16),
          children: [
            const Text(
              'الإعدادات',
              style: TextStyle(
                fontSize: 24,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 16),
            _buildSection('الحساب', [
              _buildSettingsTile(
                Icons.person,
                'الملف الشخصي',
                'تحديث معلوماتك الشخصية',
              ),
              _buildSettingsTile(
                Icons.lock,
                'الأمان',
                'كلمة المرور والمصادقة الثنائية والجلسات',
              ),
              _buildSettingsTile(
                Icons.notifications,
                'الإشعارات',
                'تكوين تفضيلات التنبيهات',
              ),
            ]),
            const SizedBox(height: 16),
            _buildSection('التفضيلات', [
              _buildSettingsTile(
                Icons.palette,
                'المظهر',
                'السمة واللغة والعرض',
              ),
              _buildSettingsTile(
                Icons.storage,
                'البيانات والتخزين',
                'إدارة البيانات المخزنة مؤقتًا',
              ),
            ]),
          ],
        ),
      ),
    );
  }

  Widget _buildSection(String title, List<Widget> children) {
    return Card(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Padding(
            padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
            child: Text(
              title,
              style: const TextStyle(
                fontWeight: FontWeight.bold,
                fontSize: 16,
              ),
            ),
          ),
          ...children,
        ],
      ),
    );
  }

  Widget _buildSettingsTile(
    IconData icon,
    String title,
    String subtitle,
  ) {
    return ListTile(
      leading: Icon(icon),
      title: Text(title),
      subtitle: Text(subtitle),
      trailing: const Icon(Icons.chevron_right),
      onTap: () {},
    );
  }
}

الخطوة 8: تجميع كل شيء معًا

أخيرًا، نقطة الدخول الرئيسية تربط السمة وغلاف التنقل التكيفي.

main.dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();

  // وضع من حافة إلى حافة لمظهر عصري
  SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
  SystemChrome.setSystemUIOverlayStyle(
    const SystemUiOverlayStyle(
      statusBarColor: Colors.transparent,
      systemNavigationBarColor: Colors.transparent,
      systemNavigationBarDividerColor: Colors.transparent,
    ),
  );

  runApp(const DashboardApp());
}

class DashboardApp extends StatelessWidget {
  const DashboardApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'لوحة تحكم متجاوبة',
      theme: DashboardTheme.lightTheme,
      debugShowCheckedModeBanner: false,
      home: const AdaptiveShell(),
    );
  }
}
مهم: في الإنتاج، فكر في استخدام حل إدارة الحالة (Provider أو Riverpod أو Bloc) بدلاً من setState لمشاركة فهرس المستخدم المحدد عبر العروض الرئيسية-التفصيلية. تستخدم الأمثلة هنا setState للوضوح، لكن التطبيقات الأكبر تستفيد من إدارة الحالة المركزية.

كيف ترتبط المفاهيم

لنراجع كيف يظهر كل مفهوم تخطيط من هذا البرنامج التعليمي في تطبيق لوحة التحكم:

  • نظام نقاط التوقف (الدرسان 1-2) — فئة Breakpoints تقود جميع القرارات المتجاوبة: نوع التنقل وأعمدة الشبكة وعرض المحتوى.
  • Flex و Row و Column (الدرس 3) — قضيب التنقل + منطقة المحتوى تستخدم Row. تخطيط تفاصيل المستخدم يستخدم Column.
  • MediaQuery (الدرس 5)MediaQuery.sizeOf(context) يوفر أبعاد الشاشة لحسابات نقاط التوقف.
  • LayoutBuilder (الدرس 6)ResponsiveBuilder يغلف نمط قراءة الحجم وحساب نوع الجهاز.
  • Slivers (الدرس 13)SliverAppBar مع البحث و SliverGrid للإحصائيات و SliverList.separated للنشاط.
  • OrientationBuilder (الدرس 14) — أعمدة الشبكة تتكيف مع الاتجاه ضمن نظام نقاط التوقف.
  • SafeArea (الدرس 15) — مطبق على كل صفحة للتعامل الصحيح مع النتوءات ومؤشر الصفحة الرئيسية.
  • SystemChrome (الدرس 15) — وضع من حافة إلى حافة مع أشرطة نظام شفافة في main().
ملخص أفضل الممارسات:
1. ابدأ دائمًا بنظام نقاط توقف واستخدمه بشكل متسق.
2. استخدم تنقلًا تكيفيًا يتوافق مع اصطلاح المنصة.
3. نفّذ نمط رئيسي-تفصيلي للصفحات كثيفة البيانات على الشاشات الأكبر.
4. استخدم SliverAppBar لرؤوس التمرير الغنية.
5. لف محتوى الصفحة في SafeArea عندما لا يكون هناك AppBar.
6. عيّن وضع من حافة إلى حافة لمظهر عصري.
7. قيّد عرض المحتوى على الشاشات الكبيرة جدًا.
8. اختبر على عوامل شكل الهاتف والجهاز اللوحي وسطح المكتب.
ملخص: أظهر هذا المشروع الختامي كيفية بناء تطبيق Flutter متجاوب بالكامل من الصفر. أنشأنا أداة نقاط التوقف وغلاف التنقل التكيفي وشبكة لوحة التحكم المتجاوبة وشريط التطبيق القابل للطي مع البحث وإدارة المستخدمين بنمط رئيسي-تفصيلي ومعالجة متسقة للمناطق الآمنة. تم تطبيق كل مفهوم من برنامج تخطيطات Flutter والتصميم المتجاوب في سياق واقعي. النقطة الرئيسية هي أن تطبيقات Flutter المتجاوبة تُبنى من أدوات قابلة للتركيب وإعادة الاستخدام — حدد نقاط التوقف مرة واحدة وأنشئ عناصر غلاف تكيفية ودعها تقود قرارات التخطيط في جميع أنحاء التطبيق.

اكتمل الدرس!

تهانينا! لقد أكملت جميع الدروس في هذا البرنامج التعليمي.