إعداد Flutter والتطبيق الأول

تقنيات تصحيح الأخطاء

50 دقيقة الدرس 9 من 12

لماذا يهم تصحيح الأخطاء

تصحيح الأخطاء مهارة أساسية لكل مطور Flutter. بغض النظر عن مدى حرصك في كتابة الكود، ستظهر الأخطاء حتماً. معرفة كيفية إيجادها وإصلاحها بكفاءة هو ما يميز المطورين المنتجين عن المحبطين. في هذا الدرس، ستتعلم مجموعة شاملة من تقنيات تصحيح الأخطاء التي ستساعدك في تشخيص المشاكل وحلها بسرعة.

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

print() و debugPrint()

أبسط تقنية لتصحيح الأخطاء هي طباعة القيم في وحدة التحكم. يوفر Dart دالتين رئيسيتين لهذا الغرض.

استخدام print()

تقوم دالة print() بإخراج نص إلى وحدة التحكم. تعمل للفحوصات السريعة لكن لها قيود مع المخرجات الطويلة.

تصحيح أخطاء أساسي بـ print()

void main() {
  String userName = 'Edrees';
  int userAge = 28;
  List<String> hobbies = ['coding', 'reading', 'gaming'];

  print('User name: \$userName');
  print('User age: \$userAge');
  print('Hobbies: \$hobbies');
  print('Number of hobbies: \${hobbies.length}');
}

استخدام debugPrint()

يُفضل استخدام دالة debugPrint() في Flutter لأنها تتحكم في سرعة الإخراج لتجنب فقدان الأسطر على Android. كما أنها تتعامل مع النصوص الطويلة بشكل أفضل من print().

debugPrint() لـ Flutter

import 'package:flutter/foundation.dart';

class UserProfile extends StatelessWidget {
  final String name;
  final int age;

  const UserProfile({super.key, required this.name, required this.age});

  @override
  Widget build(BuildContext context) {
    debugPrint('Building UserProfile: name=\$name, age=\$age');
    debugPrint('Context: \${context.widget.runtimeType}');

    return Card(
      child: Column(
        children: [
          Text(name),
          Text('Age: \$age'),
        ],
      ),
    );
  }
}
نصيحة: استخدم debugPrint() بدلاً من print() في تطبيقات Flutter. يمنع فقدان المخرجات على Android ويوفر تنسيقاً أفضل للنصوص الطويلة. يمكنك أيضاً تعيين debugPrint = (String? message, {int? wrapWidth}) {}; لإسكات جميع طباعات التصحيح في الإنتاج.

نقاط التوقف في VS Code و Android Studio

تسمح لك نقاط التوقف بإيقاف برنامجك مؤقتاً عند أسطر محددة وفحص حالة جميع المتغيرات في تلك اللحظة. هذا أقوى بكثير من تصحيح الأخطاء بالطباعة.

تعيين نقاط التوقف في VS Code

في VS Code، انقر في الهامش (المساحة على يسار أرقام الأسطر) لإضافة نقطة توقف حمراء. عندما يصل تطبيقك إلى ذلك السطر، يتوقف التنفيذ مؤقتاً.

كود مع أهداف نقاط التوقف

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

  @override
  State<CounterScreen> createState() => _CounterScreenState();
}

class _CounterScreenState extends State<CounterScreen> {
  int _counter = 0;

  void _incrementCounter() {
    // عيّن نقطة توقف على السطر التالي لفحص _counter
    setState(() {
      _counter++;  // <-- نقطة توقف هنا
    });
    debugPrint('Counter incremented to: \$_counter');
  }

  @override
  Widget build(BuildContext context) {
    // عيّن نقطة توقف هنا لفحص استدعاءات البناء
    return Scaffold(
      appBar: AppBar(title: const Text('Counter')),
      body: Center(
        child: Text(
          'Count: \$_counter',
          style: Theme.of(context).textTheme.headlineMedium,
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        child: const Icon(Icons.add),
      ),
    );
  }
}

تعيين نقاط التوقف في Android Studio

في Android Studio، انقر في الهامش بجوار رقم السطر. تظهر دائرة حمراء. يمكنك أيضاً النقر بزر الماوس الأيمن على نقطة توقف لإضافة شروط.

ملاحظة: لبدء التصحيح، استخدم Run > Debug (أو اضغط F5 في VS Code / Shift+F9 في Android Studio) بدلاً من أمر التشغيل العادي. يجب أن يعمل التطبيق في وضع التصحيح حتى تعمل نقاط التوقف.

نقاط التوقف الشرطية

يمكنك تعيين نقاط توقف تُفعّل فقط عندما يكون شرط معين صحيحاً. هذا مفيد عند تصحيح الحلقات أو الطرق التي تُستدعى بشكل متكرر.

مثال على نقطة توقف شرطية

void processItems(List<Map<String, dynamic>> items) {
  for (int i = 0; i < items.length; i++) {
    var item = items[i];
    var price = item['price'] as double;

    // عيّن نقطة توقف شرطية: price < 0
    // انقر بزر الماوس الأيمن على نقطة التوقف > تحرير > شرط: price < 0
    var total = price * (item['quantity'] as int);

    debugPrint('Item \$i: price=\$price, total=\$total');
  }
}

التنقل خطوة بخطوة في الكود

بمجرد وصول برنامجك إلى نقطة توقف، يمكنك التحكم في التنفيذ خطوة بخطوة. هناك ثلاثة أوامر تنقل رئيسية:

تخطي (F10)

تخطي ينفذ السطر الحالي وينتقل إلى السطر التالي في نفس الدالة. إذا كان السطر الحالي يحتوي على استدعاء دالة، يشغّل الدالة بالكامل دون الدخول فيها.

الدخول (F11)

الدخول ينقل التنفيذ إلى داخل الدالة المُستدعاة في السطر الحالي. استخدم هذا عندما تريد رؤية ما يحدث داخل دالة.

الخروج (Shift+F11)

الخروج يستمر في التنفيذ حتى تعود الدالة الحالية، ثم يتوقف عند الدالة المُستدعية. استخدم هذا عندما تكون قد رأيت ما يكفي من الدالة الحالية.

مثال على التنقل

double calculateDiscount(double price, double percentage) {
  // الدخول يأتي بك إلى هنا
  var discount = price * (percentage / 100);
  var finalPrice = price - discount;
  return finalPrice;  // الخروج يعود إلى المُستدعي
}

void processOrder() {
  var price = 99.99;
  var discount = 15.0;

  // تخطي: يشغّل calculateDiscount دون الدخول فيها
  // الدخول: يدخل دالة calculateDiscount
  var finalPrice = calculateDiscount(price, discount);

  // بعد التخطي أو الخروج، يستمر التنفيذ هنا
  debugPrint('Final price: \$finalPrice');
}

تعبيرات المراقبة

تتيح لك تعبيرات المراقبة مراقبة متغيرات أو تعبيرات محددة أثناء التنقل في الكود. تتحدث تلقائياً عند كل نقطة توقف أو خطوة.

إضافة تعبيرات المراقبة

في شريط التصحيح الجانبي (VS Code) أو نافذة أدوات التصحيح (Android Studio)، ابحث عن قسم المراقبة وانقر على زر + لإضافة تعبيرات.

تعبيرات مراقبة مفيدة

// بالنظر لهذا الكود المتوقف عند نقطة توقف:
class ShoppingCart {
  List<CartItem> items = [];

  double get totalPrice =>
      items.fold(0, (sum, item) => sum + item.price * item.quantity);

  void addItem(CartItem item) {
    items.add(item);  // <-- نقطة توقف هنا
  }
}

// تعبيرات مراقبة مفيدة:
// items.length
// totalPrice
// items.last.price
// items.where((i) => i.quantity > 1).toList()
// items.map((i) => i.name).toList()

تقييم التعبيرات

تتيح لك ميزة تقييم التعبيرات تشغيل أي تعبير Dart أثناء التوقف عند نقطة توقف. هذا مثل وجود REPL داخل تطبيقك قيد التشغيل.

أمثلة على تقييم التعبيرات

// أثناء التوقف عند نقطة توقف، افتح تقييم التعبيرات:
// VS Code: وحدة تحكم التصحيح (اكتب التعبيرات مباشرة)
// Android Studio: Run > Evaluate Expression (Alt+F8)

// أمثلة على التعبيرات التي يمكنك تقييمها:
// items.length
// items.where((i) => i.price > 50).toList()
// jsonEncode(items.first.toJson())
// MediaQuery.of(context).size
// Theme.of(context).colorScheme.primary
نصيحة: في VS Code، تعمل وحدة تحكم التصحيح في أسفل الشاشة كـ REPL تفاعلي. يمكنك كتابة أي تعبير Dart أثناء التوقف ورؤية النتيجة فوراً. هذه واحدة من أقوى ميزات التصحيح المتاحة.

وحدة تحكم التصحيح

تعرض وحدة تحكم التصحيح جميع المخرجات من استدعاءات print() و debugPrint()، بالإضافة إلى أي تعبيرات تقوم بتقييمها. كما تعرض رسائل وتحذيرات إطار عمل Flutter.

أمثلة على مخرجات وحدة تحكم التصحيح

// مخرجات وحدة تحكم التصحيح النموذجية:
// flutter: Building UserProfile: name=Edrees, age=28
// flutter: Counter incremented to: 1
// flutter: ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══
// flutter: The following assertion was thrown building...
// flutter: RenderBox was not laid out: RenderFlex#abc123
// flutter: 'package:flutter/src/rendering/box.dart':
// flutter: Failed assertion: line 1972 pos 12: 'hasSize'

تعليمات Assert

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

استخدام Assert للبرمجة الدفاعية

class BankAccount {
  double _balance;

  BankAccount(this._balance) {
    assert(_balance >= 0, 'Initial balance cannot be negative');
  }

  void deposit(double amount) {
    assert(amount > 0, 'Deposit amount must be positive: \$amount');
    _balance += amount;
  }

  void withdraw(double amount) {
    assert(amount > 0, 'Withdrawal amount must be positive: \$amount');
    assert(amount <= _balance,
        'Insufficient funds: tried \$amount but balance is \$_balance');
    _balance -= amount;
  }

  double get balance => _balance;
}

void main() {
  var account = BankAccount(100);
  account.deposit(50);     // OK
  account.withdraw(200);   // AssertionError في وضع التصحيح!
}
تحذير: لا تستخدم أبداً assert للتحقق الذي يجب أن يحدث في الإنتاج. تتم إزالة تعليمات assert بالكامل في بناء الإصدار (flutter build). للتحقق في الإنتاج، استخدم تعليمات if وارمِ استثناءات مناسبة.

رسائل خطأ Flutter (شاشة الموت الحمراء)

عندما يواجه Flutter خطأ أثناء العرض، يعرض شاشة خطأ حمراء زاهية (في وضع التصحيح) أو شاشة رمادية (في وضع الإصدار). فهم شاشات الخطأ هذه أمر بالغ الأهمية لتصحيح الأخطاء.

قراءة الشاشة الحمراء

تعرض الشاشة الحمراء ثلاث معلومات مهمة:

  • نوع الخطأ: السطر الأول يخبرك بما حدث خطأ (مثل RenderFlex overflowed)
  • رسالة الخطأ: وصف مفصل للمشكلة
  • تتبع المكدس: سلسلة استدعاءات الدوال التي أدت إلى الخطأ

خطأ شاشة حمراء شائع

// هذا الكود يسبب خطأ تجاوز RenderFlex:
class BadLayout extends StatelessWidget {
  const BadLayout({super.key});

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        // عنصر Text هذا يمكن أن يتجاوز Row
        Text(
          'This is a very long text that will overflow '
          'the horizontal space available in this Row widget '
          'and cause a RenderFlex overflowed error',
          style: const TextStyle(fontSize: 24),
        ),
      ],
    );
  }
}

// رسالة الخطأ في وحدة التحكم:
// ══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞══
// A RenderFlex overflowed by 156 pixels on the right.
// The relevant error-causing widget was:
//   Row  <-- هذا يخبرك أي عنصر واجهة سبب الخطأ

الأخطاء الشائعة وكيفية قراءة تتبع المكدس

يُظهر تتبع المكدس تسلسل استدعاءات الدوال التي أدت إلى الخطأ. قراءته من الأعلى إلى الأسفل تُظهر لك أحدث استدعاء أولاً.

قراءة تتبع المكدس

// مثال على خطأ وتتبع المكدس:
// ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞══
// The following _TypeError was thrown building MyWidget:
// type 'Null' is not a subtype of type 'String'
//
// The relevant error-causing widget was:
//   MyWidget file:///lib/screens/home.dart:45:15
//
// When the exception was thrown, this was the stack:
// #0  MyWidget.build (package:myapp/widgets/my_widget.dart:23:20)
// #1  StatelessElement.build (package:flutter/src/widgets/framework.dart:5765:28)
// #2  ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5694:15)
//
// انظر للسطر #0 -- هذا كودك حيث حدث الخطأ
// الأسطر #1 و #2 هي كود إطار عمل Flutter (عادة تجاهلها)

أكثر أخطاء Flutter شيوعاً

خطأ Null

// Error: type 'Null' is not a subtype of type 'String'
// السبب: محاولة استخدام قيمة null كنوع غير قابل للـ null

// خطأ:
String? name;
Text(name!);  // ينهار إذا كان name هو null

// إصلاح:
Text(name ?? 'Unknown');  // توفير قيمة افتراضية

استدعاء setState() بعد dispose()

// Error: setState() called after dispose()
// السبب: عملية غير متزامنة تكتمل بعد إزالة العنصر

// خطأ:
class MyWidget extends StatefulWidget { ... }

class _MyWidgetState extends State<MyWidget> {
  void _loadData() async {
    var data = await fetchFromApi();
    setState(() {  // قد يكون العنصر قد أُزيل!
      _data = data;
    });
  }

  // إصلاح: تحقق من mounted قبل setState
  void _loadDataFixed() async {
    var data = await fetchFromApi();
    if (mounted) {
      setState(() {
        _data = data;
      });
    }
  }
}

تصحيح أخطاء تجاوز التخطيط

أخطاء تجاوز التخطيط من أكثر مشاكل Flutter شيوعاً. تحدث عندما يحاول عنصر واجهة أن يكون أكبر من المساحة المتاحة.

إصلاح أخطاء التجاوز الشائعة

// المشكلة: تجاوز Row
Row(
  children: [
    Text('Very long text that overflows...'),
  ],
)

// الحل 1: لف بـ Expanded
Row(
  children: [
    Expanded(
      child: Text(
        'Very long text that now wraps...',
        overflow: TextOverflow.ellipsis,
      ),
    ),
  ],
)

// المشكلة: تجاوز Column عند ظهور لوحة المفاتيح
Column(
  children: [
    // عناصر واجهة كثيرة تتجاوز عندما تدفعها لوحة المفاتيح للأعلى
  ],
)

// الحل: لف بـ SingleChildScrollView
SingleChildScrollView(
  child: Column(
    children: [
      // العناصر يمكنها الآن التمرير
    ],
  ),
)

// المشكلة: ارتفاع غير محدود في ListView
Column(
  children: [
    ListView(  // خطأ: Vertical viewport given unbounded height
      children: [...],
    ),
  ],
)

// الحل: لف ListView بـ Expanded
Column(
  children: [
    Expanded(
      child: ListView(
        children: [...],
      ),
    ),
  ],
)
نصيحة: فعّل طبقة Debug Paint بالضغط على p في الطرفية أثناء تشغيل تطبيقك، أو بإضافة debugPaintSizeEnabled = true; إلى كودك. هذا يُظهر حدوداً مرئية حول كل عنصر واجهة، مما يسهّل رؤية أي عنصر يسبب التجاوز.
ملاحظة: توفر مجموعة أدوات Flutter DevTools مفتش العناصر الذي يتيح لك استكشاف شجرة العناصر بصرياً ورؤية قيود الحجم لكل عنصر. يمكنك الوصول إليه من لوحة أوامر VS Code: Flutter: Open DevTools.

الملخص

في هذا الدرس، تعلمت تقنيات تصحيح الأخطاء الأساسية لتطوير Flutter:

  • استخدم debugPrint() بدلاً من print() لمخرجات وحدة تحكم موثوقة
  • عيّن نقاط توقف في بيئة التطوير لإيقاف التنفيذ وفحص الحالة
  • تنقل في الكود سطراً بسطر (تخطي، دخول، خروج)
  • تعبيرات المراقبة وتقييم التعبيرات للفحص في الوقت الفعلي
  • استخدم تعليمات assert لفحوصات الأمان في وقت التطوير
  • اقرأ رسائل خطأ Flutter وتتبع المكدس لإيجاد السبب الجذري
  • صحح أخطاء تجاوز التخطيط بلف العناصر المناسب
تحذير: أزل أو عطّل جميع تعليمات debugPrint() وكود التصحيح فقط قبل إصدار تطبيقك. بينما تتم إزالة تعليمات assert تلقائياً، لا تتم إزالة تعليمات الطباعة ويمكن أن تؤثر على الأداء.