أساسيات إدارة الحالة

بناء تطبيق ذي حالة مع Provider

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

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

في درس مشروع التخرج هذا سنبني تطبيق مهام كامل بجودة إنتاجية من الصفر باستخدام Provider. يوضح هذا التطبيق كل أفضل الممارسات التي تعلمناها: الحالة غير القابلة للتغيير ومصدر واحد للحقيقة وفصل الاهتمامات وتحسين إعادة البناء المناسب واستمرارية الحالة. في النهاية سيكون لديك تطبيق يعمل بالكامل مع إضافة/تعديل/حذف المهام والتصفية والبحث والإحصائيات وتبديل المظهر والتخزين المستمر.

ما سنبنيه:
- إضافة وتعديل وحذف المهام
- التصفية حسب: الكل والنشطة والمكتملة
- البحث في المهام حسب العنوان
- إحصائيات عدد المهام (الإجمالي والنشطة والمكتملة)
- تبديل المظهر الداكن/الفاتح مع الاستمرارية
- استمرارية الحالة باستخدام SharedPreferences
- إعداد MultiProvider مع عدة ChangeNotifiers
- ودجات Consumer و Selector لإعادة بناء مستهدفة
- التخلص المناسب من الموارد

الخطوة 1: نماذج البيانات

نبدأ بنماذج بيانات غير قابلة للتغيير. كل نموذج يستخدم copyWith للتحديثات الآمنة ويتضمن تسلسل JSON للاستمرارية.

lib/models/todo.dart

import 'dart:convert';

enum FilterType { all, active, completed }

class Todo {
  final String id;
  final String title;
  final String description;
  final bool isCompleted;
  final DateTime createdAt;
  final DateTime? completedAt;

  const Todo({
    required this.id,
    required this.title,
    this.description = '',
    this.isCompleted = false,
    required this.createdAt,
    this.completedAt,
  });

  // تحديث غير قابل للتغيير -- يُرجع Todo جديد ولا يغير أبداً
  Todo copyWith({
    String? title,
    String? description,
    bool? isCompleted,
    DateTime? completedAt,
  }) {
    return Todo(
      id: id,
      title: title ?? this.title,
      description: description ?? this.description,
      isCompleted: isCompleted ?? this.isCompleted,
      createdAt: createdAt,
      completedAt: completedAt ?? this.completedAt,
    );
  }

  // تسلسل JSON للاستمرارية
  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'title': title,
      'description': description,
      'isCompleted': isCompleted,
      'createdAt': createdAt.toIso8601String(),
      'completedAt': completedAt?.toIso8601String(),
    };
  }

  factory Todo.fromJson(Map<String, dynamic> json) {
    return Todo(
      id: json['id'] as String,
      title: json['title'] as String,
      description: json['description'] as String? ?? '',
      isCompleted: json['isCompleted'] as bool? ?? false,
      createdAt: DateTime.parse(json['createdAt'] as String),
      completedAt: json['completedAt'] != null
          ? DateTime.parse(json['completedAt'] as String)
          : null,
    );
  }

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is Todo && runtimeType == other.runtimeType && id == other.id;

  @override
  int get hashCode => id.hashCode;
}

الخطوة 2: مُعلم حالة المهام

TodoNotifier هو المصدر الواحد للحقيقة لجميع بيانات المهام. يدير القائمة والتصفية والبحث والاستمرارية. لاحظ كيف أن كل طريقة تنشئ حالة جديدة بدلاً من تغيير الحالة الموجودة.

lib/providers/todo_notifier.dart

import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/todo.dart';

class TodoNotifier extends ChangeNotifier {
  List<Todo> _todos = [];
  FilterType _filter = FilterType.all;
  String _searchQuery = '';
  bool _isLoading = true;

  // مُحصلات عامة -- وصول قراءة فقط للحالة
  List<Todo> get todos => List.unmodifiable(_todos);
  FilterType get filter => _filter;
  String get searchQuery => _searchQuery;
  bool get isLoading => _isLoading;

  // حالة مشتقة -- محسوبة ولا تُخزن أبداً
  int get totalCount => _todos.length;
  int get activeCount => _todos.where((t) => !t.isCompleted).length;
  int get completedCount => _todos.where((t) => t.isCompleted).length;

  // المهام المفلترة + المبحوثة
  List<Todo> get filteredTodos {
    List<Todo> result = _todos;

    // تطبيق الفلتر
    switch (_filter) {
      case FilterType.active:
        result = result.where((t) => !t.isCompleted).toList();
        break;
      case FilterType.completed:
        result = result.where((t) => t.isCompleted).toList();
        break;
      case FilterType.all:
        break;
    }

    // تطبيق البحث
    if (_searchQuery.isNotEmpty) {
      final query = _searchQuery.toLowerCase();
      result = result.where((t) =>
        t.title.toLowerCase().contains(query) ||
        t.description.toLowerCase().contains(query)
      ).toList();
    }

    return result;
  }

  // ====== الإجراءات ======

  Future<void> loadTodos() async {
    _isLoading = true;
    notifyListeners();

    try {
      final prefs = await SharedPreferences.getInstance();
      final todosJson = prefs.getString('todos');

      if (todosJson != null) {
        final List<dynamic> decoded = jsonDecode(todosJson);
        _todos = decoded
            .map((item) => Todo.fromJson(item as Map<String, dynamic>))
            .toList();
      }
    } catch (e) {
      debugPrint('خطأ في تحميل المهام: \$e');
    }

    _isLoading = false;
    notifyListeners();
  }

  Future<void> addTodo(String title, {String description = ''}) async {
    final todo = Todo(
      id: DateTime.now().millisecondsSinceEpoch.toString(),
      title: title.trim(),
      description: description.trim(),
      createdAt: DateTime.now(),
    );

    // أنشئ قائمة جديدة مع المهمة الجديدة
    _todos = [..._todos, todo];
    notifyListeners();
    await _saveTodos();
  }

  Future<void> updateTodo(String id, {String? title, String? description}) async {
    _todos = _todos.map((todo) {
      if (todo.id == id) {
        return todo.copyWith(
          title: title?.trim(),
          description: description?.trim(),
        );
      }
      return todo;
    }).toList();

    notifyListeners();
    await _saveTodos();
  }

  Future<void> toggleTodo(String id) async {
    _todos = _todos.map((todo) {
      if (todo.id == id) {
        return todo.copyWith(
          isCompleted: !todo.isCompleted,
          completedAt: !todo.isCompleted ? DateTime.now() : null,
        );
      }
      return todo;
    }).toList();

    notifyListeners();
    await _saveTodos();
  }

  Future<void> deleteTodo(String id) async {
    _todos = _todos.where((todo) => todo.id != id).toList();
    notifyListeners();
    await _saveTodos();
  }

  Future<void> clearCompleted() async {
    _todos = _todos.where((todo) => !todo.isCompleted).toList();
    notifyListeners();
    await _saveTodos();
  }

  void setFilter(FilterType newFilter) {
    if (_filter != newFilter) {
      _filter = newFilter;
      notifyListeners();
    }
  }

  void setSearchQuery(String query) {
    if (_searchQuery != query) {
      _searchQuery = query;
      notifyListeners();
    }
  }

  // ====== الاستمرارية ======

  Future<void> _saveTodos() async {
    try {
      final prefs = await SharedPreferences.getInstance();
      final todosJson = jsonEncode(_todos.map((t) => t.toJson()).toList());
      await prefs.setString('todos', todosJson);
    } catch (e) {
      debugPrint('خطأ في حفظ المهام: \$e');
    }
  }
}

الخطوة 3: مُعلم المظهر

مُعلم منفصل يتعامل مع حالة المظهر. هذا يتبع فصل الاهتمامات -- منطق المهام ومنطق المظهر مستقلان تماماً.

lib/providers/theme_notifier.dart

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

class ThemeNotifier extends ChangeNotifier {
  ThemeMode _themeMode = ThemeMode.light;

  ThemeMode get themeMode => _themeMode;
  bool get isDarkMode => _themeMode == ThemeMode.dark;

  Future<void> loadTheme() async {
    try {
      final prefs = await SharedPreferences.getInstance();
      final isDark = prefs.getBool('isDarkMode') ?? false;
      _themeMode = isDark ? ThemeMode.dark : ThemeMode.light;
      notifyListeners();
    } catch (e) {
      debugPrint('خطأ في تحميل المظهر: \$e');
    }
  }

  Future<void> toggleTheme() async {
    _themeMode = isDarkMode ? ThemeMode.light : ThemeMode.dark;
    notifyListeners();

    try {
      final prefs = await SharedPreferences.getInstance();
      await prefs.setBool('isDarkMode', isDarkMode);
    } catch (e) {
      debugPrint('خطأ في حفظ المظهر: \$e');
    }
  }
}

الخطوة 4: نقطة دخول التطبيق مع MultiProvider

نستخدم MultiProvider في جذر التطبيق لتوفير كلا المُعلمين لشجرة الودجات بأكملها. رد نداء create يُشغل أيضاً تحميل البيانات الأولي.

lib/main.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'providers/todo_notifier.dart';
import 'providers/theme_notifier.dart';
import 'screens/home_screen.dart';

void main() {
  runApp(const TodoApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(
          create: (_) {
            final notifier = TodoNotifier();
            notifier.loadTodos();  // تحميل المهام المحفوظة عند بدء التشغيل
            return notifier;
          },
        ),
        ChangeNotifierProvider(
          create: (_) {
            final notifier = ThemeNotifier();
            notifier.loadTheme();  // تحميل المظهر المحفوظ عند بدء التشغيل
            return notifier;
          },
        ),
      ],
      child: Consumer<ThemeNotifier>(
        builder: (context, themeNotifier, child) {
          return MaterialApp(
            title: 'تطبيق المهام',
            debugShowCheckedModeBanner: false,
            themeMode: themeNotifier.themeMode,
            theme: ThemeData(
              colorSchemeSeed: Colors.blue,
              useMaterial3: true,
              brightness: Brightness.light,
            ),
            darkTheme: ThemeData(
              colorSchemeSeed: Colors.blue,
              useMaterial3: true,
              brightness: Brightness.dark,
            ),
            home: const HomeScreen(),
          );
        },
      ),
    );
  }
}

الخطوة 5: الشاشة الرئيسية مع الإحصائيات والبحث

تجمع الشاشة الرئيسية كل شيء: شريط الإحصائيات والبحث وشرائح الفلتر وقائمة المهام. لاحظ كيف نستخدم Selector للإحصائيات لتجنب إعادة بناء الشاشة بأكملها عندما يتغير العدد فقط.

lib/screens/home_screen.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/todo.dart';
import '../providers/todo_notifier.dart';
import '../providers/theme_notifier.dart';
import '../widgets/todo_list.dart';
import '../widgets/stats_bar.dart';
import '../widgets/add_todo_sheet.dart';

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

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  final _searchController = TextEditingController();

  @override
  void dispose() {
    _searchController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('مهامي'),
        actions: [
          // تبديل المظهر -- يستخدم Consumer لإعادة بناء مستهدفة
          Consumer<ThemeNotifier>(
            builder: (context, theme, _) {
              return IconButton(
                icon: Icon(
                  theme.isDarkMode ? Icons.light_mode : Icons.dark_mode,
                ),
                onPressed: theme.toggleTheme,
                tooltip: theme.isDarkMode
                    ? 'التبديل إلى الوضع الفاتح'
                    : 'التبديل إلى الوضع الداكن',
              );
            },
          ),
          // زر مسح المكتملة
          PopupMenuButton<String>(
            onSelected: (value) {
              if (value == 'clear_completed') {
                context.read<TodoNotifier>().clearCompleted();
              }
            },
            itemBuilder: (context) => [
              const PopupMenuItem(
                value: 'clear_completed',
                child: Text('مسح المكتملة'),
              ),
            ],
          ),
        ],
      ),
      body: Column(
        children: [
          // شريط الإحصائيات -- يعيد البناء فقط عند تغير الأعداد
          const StatsBar(),

          // حقل البحث
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
            child: TextField(
              controller: _searchController,
              decoration: InputDecoration(
                hintText: 'البحث في المهام...',
                prefixIcon: const Icon(Icons.search),
                suffixIcon: _searchController.text.isNotEmpty
                    ? IconButton(
                        icon: const Icon(Icons.clear),
                        onPressed: () {
                          _searchController.clear();
                          context.read<TodoNotifier>().setSearchQuery('');
                        },
                      )
                    : null,
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.circular(12),
                ),
              ),
              onChanged: (value) {
                context.read<TodoNotifier>().setSearchQuery(value);
                setState(() {});  // تحديث رؤية أيقونة اللاحقة
              },
            ),
          ),

          // شرائح الفلتر
          const FilterChips(),

          // قائمة المهام
          const Expanded(child: TodoList()),
        ],
      ),
      floatingActionButton: FloatingActionButton.extended(
        onPressed: () => _showAddTodoSheet(context),
        icon: const Icon(Icons.add),
        label: const Text('إضافة مهمة'),
      ),
    );
  }

  void _showAddTodoSheet(BuildContext context) {
    showModalBottomSheet(
      context: context,
      isScrollControlled: true,
      builder: (_) => const AddTodoSheet(),
    );
  }
}

// شرائح الفلتر -- تعيد البناء فقط عند تغير الفلتر
class FilterChips extends StatelessWidget {
  const FilterChips({super.key});

  @override
  Widget build(BuildContext context) {
    // استخدام Selector لإعادة البناء فقط عند تغير الفلتر
    final currentFilter = context.select<TodoNotifier, FilterType>(
      (notifier) => notifier.filter,
    );

    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
      child: Row(
        children: FilterType.values.map((filter) {
          final label = switch (filter) {
            FilterType.all => 'الكل',
            FilterType.active => 'النشطة',
            FilterType.completed => 'المكتملة',
          };

          return Padding(
            padding: const EdgeInsets.only(right: 8),
            child: FilterChip(
              label: Text(label),
              selected: currentFilter == filter,
              onSelected: (_) {
                context.read<TodoNotifier>().setFilter(filter);
              },
            ),
          );
        }).toList(),
      ),
    );
  }
}

الخطوة 6: ودجت شريط الإحصائيات

يستخدم شريط الإحصائيات Selector لإعادة البناء فقط عندما تتغير قيم العدد الفعلية -- ليس عند تعديل عنوان مهمة أو تغيير الفلتر.

lib/widgets/stats_bar.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/todo_notifier.dart';

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

  @override
  Widget build(BuildContext context) {
    // Selector يعيد البناء فقط عند تغير (الإجمالي والنشطة والمكتملة)
    return Selector<TodoNotifier, ({int total, int active, int completed})>(
      selector: (_, notifier) => (
        total: notifier.totalCount,
        active: notifier.activeCount,
        completed: notifier.completedCount,
      ),
      builder: (context, counts, child) {
        return Container(
          padding: const EdgeInsets.all(16),
          margin: const EdgeInsets.all(16),
          decoration: BoxDecoration(
            color: Theme.of(context).colorScheme.primaryContainer,
            borderRadius: BorderRadius.circular(12),
          ),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [
              _StatItem(
                label: 'الإجمالي',
                count: counts.total,
                icon: Icons.list,
              ),
              _StatItem(
                label: 'النشطة',
                count: counts.active,
                icon: Icons.radio_button_unchecked,
              ),
              _StatItem(
                label: 'مكتملة',
                count: counts.completed,
                icon: Icons.check_circle_outline,
              ),
            ],
          ),
        );
      },
    );
  }
}

class _StatItem extends StatelessWidget {
  final String label;
  final int count;
  final IconData icon;

  const _StatItem({
    required this.label,
    required this.count,
    required this.icon,
  });

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Icon(icon, size: 24),
        const SizedBox(height: 4),
        Text(
          '\$count',
          style: Theme.of(context).textTheme.headlineSmall?.copyWith(
            fontWeight: FontWeight.bold,
          ),
        ),
        Text(label, style: Theme.of(context).textTheme.bodySmall),
      ],
    );
  }
}

الخطوة 7: قائمة المهام وبلاطة المهمة

تعرض القائمة المهام المفلترة مع السحب للحذف والنقر للتبديل.

lib/widgets/todo_list.dart و todo_tile.dart

// todo_list.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/todo_notifier.dart';
import 'todo_tile.dart';

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

  @override
  Widget build(BuildContext context) {
    return Consumer<TodoNotifier>(
      builder: (context, notifier, _) {
        if (notifier.isLoading) {
          return const Center(child: CircularProgressIndicator());
        }

        final todos = notifier.filteredTodos;

        if (todos.isEmpty) {
          return Center(
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                Icon(Icons.inbox, size: 64,
                  color: Theme.of(context).colorScheme.outline),
                const SizedBox(height: 16),
                Text(
                  notifier.searchQuery.isNotEmpty
                      ? 'لا توجد مهام تطابق بحثك'
                      : notifier.filter == FilterType.all
                          ? 'لا توجد مهام بعد. أضف واحدة!'
                          : 'لا توجد مهام \${notifier.filter.name}',
                  style: Theme.of(context).textTheme.bodyLarge?.copyWith(
                    color: Theme.of(context).colorScheme.outline,
                  ),
                ),
              ],
            ),
          );
        }

        return ListView.builder(
          padding: const EdgeInsets.only(bottom: 80),
          itemCount: todos.length,
          itemBuilder: (context, index) {
            return TodoTile(
              key: ValueKey(todos[index].id),
              todo: todos[index],
            );
          },
        );
      },
    );
  }
}

// todo_tile.dart
import 'package:flutter/material.dart';
import '../models/todo.dart';
import '../providers/todo_notifier.dart';
import 'package:provider/provider.dart';

class TodoTile extends StatelessWidget {
  final Todo todo;

  const TodoTile({super.key, required this.todo});

  @override
  Widget build(BuildContext context) {
    return Dismissible(
      key: ValueKey(todo.id),
      direction: DismissDirection.endToStart,
      background: Container(
        alignment: Alignment.centerRight,
        padding: const EdgeInsets.only(right: 20),
        color: Colors.red,
        child: const Icon(Icons.delete, color: Colors.white),
      ),
      confirmDismiss: (_) async {
        return await showDialog<bool>(
          context: context,
          builder: (ctx) => AlertDialog(
            title: const Text('حذف المهمة'),
            content: Text('حذف "\${todo.title}"؟'),
            actions: [
              TextButton(
                onPressed: () => Navigator.pop(ctx, false),
                child: const Text('إلغاء'),
              ),
              TextButton(
                onPressed: () => Navigator.pop(ctx, true),
                child: const Text('حذف',
                  style: TextStyle(color: Colors.red)),
              ),
            ],
          ),
        );
      },
      onDismissed: (_) {
        context.read<TodoNotifier>().deleteTodo(todo.id);
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('"\${todo.title}" حُذفت')),
        );
      },
      child: ListTile(
        leading: Checkbox(
          value: todo.isCompleted,
          onChanged: (_) =>
              context.read<TodoNotifier>().toggleTodo(todo.id),
        ),
        title: Text(
          todo.title,
          style: TextStyle(
            decoration: todo.isCompleted
                ? TextDecoration.lineThrough
                : null,
          ),
        ),
        subtitle: todo.description.isNotEmpty
            ? Text(
                todo.description,
                maxLines: 1,
                overflow: TextOverflow.ellipsis,
              )
            : null,
        onTap: () => _showEditDialog(context),
        onLongPress: () =>
            context.read<TodoNotifier>().toggleTodo(todo.id),
      ),
    );
  }

  void _showEditDialog(BuildContext context) {
    final titleCtrl = TextEditingController(text: todo.title);
    final descCtrl = TextEditingController(text: todo.description);

    showDialog(
      context: context,
      builder: (ctx) => AlertDialog(
        title: const Text('تعديل المهمة'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            TextField(
              controller: titleCtrl,
              decoration: const InputDecoration(labelText: 'العنوان'),
              autofocus: true,
            ),
            const SizedBox(height: 8),
            TextField(
              controller: descCtrl,
              decoration: const InputDecoration(labelText: 'الوصف'),
              maxLines: 3,
            ),
          ],
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(ctx),
            child: const Text('إلغاء'),
          ),
          FilledButton(
            onPressed: () {
              if (titleCtrl.text.trim().isNotEmpty) {
                context.read<TodoNotifier>().updateTodo(
                  todo.id,
                  title: titleCtrl.text,
                  description: descCtrl.text,
                );
                Navigator.pop(ctx);
              }
            },
            child: const Text('حفظ'),
          ),
        ],
      ),
    );
  }
}

الخطوة 8: صفحة إضافة المهمة السفلية

صفحة سفلية مع معالجة نماذج مناسبة لإضافة مهام جديدة.

lib/widgets/add_todo_sheet.dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/todo_notifier.dart';

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

  @override
  State<AddTodoSheet> createState() => _AddTodoSheetState();
}

class _AddTodoSheetState extends State<AddTodoSheet> {
  final _formKey = GlobalKey<FormState>();
  final _titleController = TextEditingController();
  final _descController = TextEditingController();

  @override
  void dispose() {
    _titleController.dispose();
    _descController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      // التعديل للوحة المفاتيح
      padding: EdgeInsets.only(
        bottom: MediaQuery.of(context).viewInsets.bottom,
        left: 16,
        right: 16,
        top: 16,
      ),
      child: Form(
        key: _formKey,
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            // شريط المقبض
            Center(
              child: Container(
                width: 40,
                height: 4,
                margin: const EdgeInsets.only(bottom: 16),
                decoration: BoxDecoration(
                  color: Theme.of(context).colorScheme.outline,
                  borderRadius: BorderRadius.circular(2),
                ),
              ),
            ),
            Text(
              'مهمة جديدة',
              style: Theme.of(context).textTheme.titleLarge,
            ),
            const SizedBox(height: 16),
            TextFormField(
              controller: _titleController,
              decoration: const InputDecoration(
                labelText: 'ما الذي يجب القيام به؟',
                border: OutlineInputBorder(),
              ),
              autofocus: true,
              validator: (value) {
                if (value == null || value.trim().isEmpty) {
                  return 'الرجاء إدخال عنوان';
                }
                return null;
              },
            ),
            const SizedBox(height: 12),
            TextFormField(
              controller: _descController,
              decoration: const InputDecoration(
                labelText: 'الوصف (اختياري)',
                border: OutlineInputBorder(),
              ),
              maxLines: 3,
            ),
            const SizedBox(height: 16),
            FilledButton.icon(
              onPressed: _submit,
              icon: const Icon(Icons.add),
              label: const Text('إضافة مهمة'),
            ),
            const SizedBox(height: 16),
          ],
        ),
      ),
    );
  }

  void _submit() {
    if (_formKey.currentState!.validate()) {
      context.read<TodoNotifier>().addTodo(
        _titleController.text,
        description: _descController.text,
      );
      Navigator.pop(context);
    }
  }
}

ملخص الهندسة المعمارية

هيكل المشروع:

lib/
  main.dart -- إعداد MultiProvider ونقطة دخول التطبيق
  models/
    todo.dart -- نموذج Todo غير قابل للتغيير مع copyWith و JSON
  providers/
    todo_notifier.dart -- كل منطق أعمال المهام + الاستمرارية
    theme_notifier.dart -- تبديل المظهر + الاستمرارية
  screens/
    home_screen.dart -- الشاشة الرئيسية مع البحث والفلاتر و FAB
  widgets/
    stats_bar.dart -- إحصائيات محسنة مع Selector
    todo_list.dart -- قائمة مفلترة مع Consumer
    todo_tile.dart -- بلاطة فردية مع السحب والتعديل
    add_todo_sheet.dart -- صفحة نموذج سفلية للمهام الجديدة
الأنماط الرئيسية المستخدمة:
1. MultiProvider في الجذر للحالة العامة (TodoNotifier + ThemeNotifier)
2. Consumer للأقسام التي تحتاج المُعلم الكامل (قائمة المهام وتبديل المظهر)
3. Selector لإعادة البناء المستهدفة (شريط الإحصائيات يعيد البناء فقط عند تغير الأعداد)
4. context.read في ردود النداء (لا يعيد البناء أبداً فقط يرسل الإجراءات)
5. context.select في طرق البناء لاشتراكات دقيقة (شرائح الفلتر)
6. نماذج غير قابلة للتغيير مع copyWith في كل مكان
7. التخلص المناسب من TextEditingControllers
8. SharedPreferences للاستمرارية (يُحمل عند بدء التشغيل ويُحفظ عند كل تغيير)
أخطاء شائعة يجب تجنبها:
- لا تستخدم context.watch داخل ردود النداء -- استخدم context.read
- لا تنسَ ValueKey على عناصر القائمة لسلوك رسوم متحركة/إعادة بناء مناسب
- لا تضع منطقاً غير متزامن في build() -- استدعه من initState أو معالجات الأحداث
- لا تنسَ التخلص من المتحكمات في StatefulWidgets
- لا تنشئ مزودين جدداً داخل build() -- استخدم دائماً رد نداء create

اكتمل الدرس!

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