قصة قصيرة :
إدارة الأخطاء في تطبيقات Flutter : تخيل أنك تبني تطبيقاً بسيطاً على هاتفك باستخدام Flutter، مثل تطبيق يخزن معلومات المستخدمين أو يرسل بيانات إلى الإنترنت. هذا التطبيق يعتمد على أدوات خارجية مثل Supabase (لقواعد البيانات عبر الإنترنت)، Firebase (للخدمات السحابية)، وHive (لتخزين البيانات محلياً على الجهاز). في بعض الأحيان، قد تحدث أخطاء غير متوقعة، مثل عدم الاتصال بالإنترنت أو مشكلة في قراءة البيانات.
السيناريو هنا هو بناء “خدمة” خاصة تسمى “إدارة الأخطاء”، تعمل مثل حارس يراقب هذه الأخطاء ويتعامل معها بدلاً من أن يتوقف التطبيق فجأة. الفكرة الأساسية هي:
- الجزء الرئيسي (ِAbstract Class ): هناك فئة عامة تسمى ErrorHandler، وهي مثل خطة عامة تقول: “إذا حدث خطأ، قم بتحويله إلى شيء يمكن فهمه ومعالجته”. هذه الفئة لا تفعل شيئاً بنفسها، لكنها توفر القواعد للأجزاء الأخرى.
- الأجزاء الفرعية: لكل أداة (مثل Supabase أو Firebase)، نصنع فئة صغيرة ترث من ErrorHandler وتتعامل مع أخطاء تلك الأداة بشكل خاص. على سبيل المثال، إذا فشل Supabase، تحول الخطأ إلى رسالة واضحة مثل “مشكلة في الخادم”.
- كيفية الاستخدام: في كود التطبيق، عندما تقوم بعملية قد تفشل (مثل جلب بيانات)، تضعها داخل دالة آمنة تسمى safeCall. إذا نجحت، تعطيك النتيجة الصحيحة. إذا فشلت، تعطيك وصفاً للخطأ باستخدام مكتبة Dartz، التي تفصل بين “النجاح” و”الفشل” بطريقة مرتبة.
- المدير: هناك فئة تسمى ErrorManager تساعد في اختيار الفئة الفرعية المناسبة تلقائياً، مثل قول: “هذا خطأ من Supabase، استخدم معالج Supabase”.
بهذه الطريقة، يصبح تطبيقك أقوى وأسهل في الإصلاح، ويمكنك إضافة دعم لأدوات جديدة في المستقبل دون تغيير الكثير من الكود. هذا يشبه بناء منزل مع أبواب طوارئ، حيث يستمر المنزل في العمل حتى لو حدث حريق صغير.
المقدمة: أهمية إدارة الأخطاء في البرمجة
في مجال تطوير البرمجيات، تعد عملية إدارة الأخطاء (Error Handling) عنصراً حاسماً لضمان استقرار التطبيقات وموثوقيتها. بدون آلية فعالة للتعامل مع الأخطاء، قد يتعرض البرنامج للانهيار المفاجئ، مما يؤدي إلى فقدان البيانات أو تجربة مستخدم سيئة. في سياق إطار Flutter، الذي يعتمد على لغة Dart، تبرز أهمية هذه العملية بشكل خاص نظراً لتكامل التطبيقات مع خدمات خارجية مثل قواعد البيانات (Supabase، Firebase) والتخزين المحلي (Hive).
تساهم إدارة الأخطاء الفعالة في تحقيق عدة أهداف رئيسية:
- تعزيز الاستقرار: تمكن من التعامل مع الحالات غير المتوقعة، مثل فشل الاتصال بالشبكة أو أخطاء في قراءة البيانات، دون إيقاف التطبيق كلياً.
- تحسين الأمان: تمنع تسرب المعلومات الحساسة أثناء معالجة الأخطاء وتحمي من الثغرات الأمنية.
- التوسعية والصيانة: تسمح بإضافة دعم لأخطاء جديدة دون إعادة كتابة الكود الأساسي، مما يجعل التطبيق أكثر مرونة للتطورات المستقبلية.
- تحليل الأداء: تسهل تسجيل الأخطاء لمراقبتها وتحسين الجودة العامة للبرنامج.
في هذا المقال، سنقدم نموذجاً عملياً لخدمة إدارة الأخطاء مبنية على فئة مجردة (Abstract Class) وفئات فرعية، مع استخدام مكتبة Dartz لفصل النتائج الناجحة عن الفاشلة بطريقة وظيفية. هذا التصميم يتبع أفضل الممارسات مثل مبدأ التبعية المعكوسة (Dependency Inversion Principle) وفصل المسؤوليات (Separation of Concerns)، مما يجعله مناسباً لأي مطور يرغب في دمجه مباشرة في مشروعه. الكود المقدم جاهز للنسخ واللصق، مع شرح مفصل لكل قسم.
التصميم العام للخدمة

يعتمد التصميم على:
- فئة مجردة (ErrorHandler): توفر واجهة عامة لمعالجة الأخطاء.
- فئات فرعية: كل واحدة مخصصة لمكتبة معينة (مثل Supabase أو Firebase).
- استخدام Dartz: لإرجاع نتائج من نوع Either<Failure, T>، حيث يمثل Left<Failure> الفشل وRight<T> النجاح.
- مدير الأخطاء (ErrorManager): يساعد في اختيار الـ Handler المناسب.
للبدء، أضف التبعيات التالية إلى ملف pubspec.yaml في مشروعك:
YAML
dependencies:
dartz: ^0.10.1 # للبرمجة الوظيفية
supabase_flutter: ^2.0.0 # إذا كنت تستخدم Supabase
firebase_core: ^2.0.0 # إذا كنت تستخدم Firebase
hive: ^2.0.0 # إذا كنت تستخدم Hive
# اختياري: logger: ^1.1.0 للتسجيل
الآن، دعونا نستعرض الكود خطوة بخطوة مع الشرح.
1. تعريف فئات الفشل (Failures)
تبدأ الخدمة بتعريف Abstract Class للفشل، مع Classes فرعية لتصنيف كل نوع خطأ بدقة. هذا يسمح بعرض رسائل مخصصة أو تسجيلها بشكل منهجي.
Failures هي فئة مجردة (abstract class) تعمل كقالب عام لجميع أنواع الأخطاء المحتملة في التطبيق. لا تقوم هذه الفئة بتنفيذ أي عمليات محددة بنفسها، بل توفر هيكلاً مشتركاً يشمل خصائص أساسية مثل الرسالة الوصفية للخطأ (message) ورمز الخطأ الاختياري (code). هذا الهيكل يضمن توحيد طريقة التعامل مع الأخطاء عبر التطبيق بأكمله، مما يسهل عرض رسائل واضحة للمستخدم أو تسجيل التفاصيل لأغراض التصحيح.
Dart
import 'package:dartz/dartz.dart';
// فئة مجردة لتمثيل الفشل العام
abstract class Failure {
final String message;
final int? code;
Failure(this.message, {this.code});
}
// فئات فرعية لأنواع محددة من الفشل
class ServerFailure extends Failure {
ServerFailure(String message, {int? code}) : super(message, code: code);
}
class DatabaseFailure extends Failure {
DatabaseFailure(String message, {int? code}) : super(message, code: code);
}
class CacheFailure extends Failure {
CacheFailure(String message, {int? code}) : super(message, code: code);
}
class UnknownFailure extends Failure {
UnknownFailure(String message) : super(message);
}
الشرح:
- Failure هي الفئة الأساسية التي تحتوي على رسالة الخطأ ورمز اختياري.
- الفئات الفرعية مثل ServerFailure تُستخدم لتمييز أخطاء الخادم عن أخطاء التخزين، مما يسهل التعامل معها في واجهة المستخدم (مثل عرض تنبيهات مختلفة).
2. الفئة المجردة لإدارة الأخطاء (ErrorHandler)
هذه الفئة توفر الواجهة الرئيسية للخدمة، ويمكن توسيعها بسهولة.
Dart
abstract class ErrorHandler {
// دالة لمعالجة الاستثناء وإرجاع فشل
Failure handleError(dynamic error);
// دالة مساعدة لتغليف الدوال غير المتزامنة (اختياري، لتسهيل الاستخدام)
Future<Either<Failure, T>> safeCall<T>(Future<T> Function() call) async {
try {
final result = await call();
return Right(result);
} catch (e) {
return Left(handleError(e));
}
}
}
الشرح:
- handleError تحول أي استثناء إلى كائن Failure مناسب.
- safeCall تغلف أي دالة غير متزامنة لإرجاع Either، مما يبسط الاستخدام في الكود الرئيسي ويمنع رمي الاستثناءات غير المتوقعة.
3. الفئات الفرعية لكل مكتبة
كل فئة ترث من ErrorHandler وتنفذ handleError بناءً على أخطاء المكتبة الخاصة بها. يمكن إضافة فئات جديدة لمكتبات أخرى دون تعديل الكود القائم.
- SupabaseErrorHandler:
Dart
import 'package:supabase_flutter/supabase_flutter.dart';
class SupabaseErrorHandler implements ErrorHandler {
@override
Failure handleError(dynamic error) {
if (error is PostgrestException) {
return ServerFailure(error.message, code: error.code != null ? int.tryParse(error.code!) : null);
} else if (error is AuthException) {
return ServerFailure(error.message);
} else {
return UnknownFailure(error.toString());
}
// يمكن إضافة تسجيل: logger.e(error);
}
}
الشرح: تتعامل مع أخطاء Supabase الشائعة مثل مشكلات PostgreSQL أو التوثيق.
- FirebaseErrorHandler:
Dart
import 'package:firebase_core/firebase_core.dart';
import 'error_handler.dart';
import 'failures.dart';
class FirebaseErrorHandler implements ErrorHandler {
@override
Failure handleError(dynamic error) {
if (error is FirebaseException) {
// محاولة تحويل code من String إلى int
// إذا فشل التحويل، نستخدم hashCode كبديل
final int? errorCode = error.code != null
? int.tryParse(error.code!) ?? error.code!.hashCode
: null;
return ServerFailure(
error.message ?? 'Firebase error',
code: errorCode,
);
} else {
return UnknownFailure(error.toString());
}
}
}
الشرح: تركز على أخطاء Firebase، مثل مشكلات التخزين أو التوثيق.
- HiveErrorHandler:
Dart
import 'package:hive/hive.dart';
class HiveErrorHandler implements ErrorHandler {
@override
Failure handleError(dynamic error) {
if (error is HiveError) {
return CacheFailure(error.message);
} else {
return UnknownFailure(error.toString());
}
}
}
الشرح: تُعالج أخطاء التخزين المحلي في Hive.
- GeneralErrorHandler (للأخطاء العامة):
Dart
class GeneralErrorHandler implements ErrorHandler {
@override
Failure handleError(dynamic error) {
if (error is Exception) {
return UnknownFailure(error.toString());
} else {
return UnknownFailure('Unexpected error: $error');
}
}
}
الشرح: تستخدم كبديل لأي خطأ غير محدد.
4. مدير الأخطاء (ErrorManager)
يساعد في اختيار الـ Handler المناسب ديناميكياً.
Dart
class ErrorManager {
static ErrorHandler getHandler(String type) {
switch (type) {
case 'supabase':
return SupabaseErrorHandler();
case 'firebase':
return FirebaseErrorHandler();
case 'hive':
return HiveErrorHandler();
default:
return GeneralErrorHandler();
}
}
}
الشرح: يعتمد على نمط المصنع (Factory Pattern) لتوفير المرونة.
مثال على الاستخدام في التطبيق
لنفترض أن لديك دالة لجلب بيانات مستخدم من Supabase:
Dart
Future<Either<Failure, User>> fetchUser() async {
final handler = ErrorManager.getHandler('supabase');
return handler.safeCall(() async {
// كود Supabase: await Supabase.instance.client.from('users').select();
return User(); // افتراضي، استبدله بكودك الفعلي
});
}
الشرح:
- في حالة النجاح، يُرجع Right<User>.
- في حالة الفشل، يُرجع Left<Failure>، ويمكن التعامل معه باستخدام fold من Dartz:Dart
result.fold( (failure) => print('Error: ${failure.message}'), // عرض الخطأ (user) => print('Success: $user'), // معالجة النجاح );
اليك الكود مجمع في كلاسات جاهزة باسم الملف :
1. اسم الملف: failures.dart
هذا الملف يحتوي على الفئة المجردة Failure وفئاتها الفرعية، التي تمثل أنواع الفشل المختلفة.
Dart
import 'package:dartz/dartz.dart';
// فئة مجردة لتمثيل الفشل العام
abstract class Failure {
final String message;
final int? code;
Failure(this.message, {this.code});
}
// فئات فرعية لأنواع محددة من الفشل
class ServerFailure extends Failure {
ServerFailure(String message, {int? code}) : super(message, code: code);
}
class DatabaseFailure extends Failure {
DatabaseFailure(String message, {int? code}) : super(message, code: code);
}
class CacheFailure extends Failure {
CacheFailure(String message, {int? code}) : super(message, code: code);
}
class UnknownFailure extends Failure {
UnknownFailure(String message) : super(message);
}
2. اسم الملف: error_handler.dart
هذا الملف يحتوي على الفئة المجردة ErrorHandler، التي توفر الواجهة العامة لمعالجة الأخطاء.
Dart
import 'package:dartz/dartz.dart';
import 'failures.dart'; // استيراد failures.dart إذا كان في نفس المجلد
abstract class ErrorHandler {
// دالة لمعالجة الاستثناء وإرجاع فشل
Failure handleError(dynamic error);
// دالة مساعدة لتغليف الدوال غير المتزامنة (اختياري، لتسهيل الاستخدام)
Future<Either<Failure, T>> safeCall<T>(Future<T> Function() call) async {
try {
final result = await call();
return Right(result);
} catch (e) {
return Left(handleError(e));
}
}
}
3. اسم الملف: supabase_error_handler.dart
هذا الملف يحتوي على الفئة SupabaseErrorHandler، المخصصة لأخطاء Supabase.
Dart
import 'package:supabase_flutter/supabase_flutter.dart';
import 'error_handler.dart'; // استيراد error_handler.dart
import 'failures.dart'; // استيراد failures.dart
class SupabaseErrorHandler implements ErrorHandler {
@override
Failure handleError(dynamic error) {
if (error is PostgrestException) {
return ServerFailure(error.message, code: error.code != null ? int.tryParse(error.code!) : null);
} else if (error is AuthException) {
return ServerFailure(error.message);
} else {
return UnknownFailure(error.toString());
}
// يمكن إضافة تسجيل: logger.e(error);
}
}
4. اسم الملف: firebase_error_handler.dart
هذا الملف يحتوي على الفئة FirebaseErrorHandler، المخصصة لأخطاء Firebase.
Dart
import 'package:firebase_core/firebase_core.dart';
import 'error_handler.dart'; // استيراد error_handler.dart
import 'failures.dart'; // استيراد failures.dart
class FirebaseErrorHandler implements ErrorHandler {
@override
Failure handleError(dynamic error) {
if (error is FirebaseException) {
return ServerFailure(error.message ?? 'Firebase error', code: error.code.hashCode);
} else {
return UnknownFailure(error.toString());
}
}
}
5. اسم الملف: hive_error_handler.dart
هذا الملف يحتوي على الفئة HiveErrorHandler، المخصصة لأخطاء Hive.
Dart
import 'package:hive/hive.dart';
import 'error_handler.dart'; // استيراد error_handler.dart
import 'failures.dart'; // استيراد failures.dart
class HiveErrorHandler implements ErrorHandler {
@override
Failure handleError(dynamic error) {
if (error is HiveError) {
return CacheFailure(error.message);
} else {
return UnknownFailure(error.toString());
}
}
}
6. اسم الملف: general_error_handler.dart
هذا الملف يحتوي على الفئة GeneralErrorHandler، المخصصة للأخطاء العامة غير المحددة.
Dart
import 'error_handler.dart'; // استيراد error_handler.dart
import 'failures.dart'; // استيراد failures.dart
class GeneralErrorHandler implements ErrorHandler {
@override
Failure handleError(dynamic error) {
if (error is Exception) {
return UnknownFailure(error.toString());
} else {
return UnknownFailure('Unexpected error: $error');
}
}
}
7. اسم الملف: error_manager.dart
هذا الملف يحتوي على الفئة ErrorManager، التي تدير اختيار الـ Handler المناسب.
Dart
import 'error_handler.dart'; // استيراد error_handler.dart
import 'supabase_error_handler.dart'; // استيراد supabase_error_handler.dart
import 'firebase_error_handler.dart'; // استيراد firebase_error_handler.dart
import 'hive_error_handler.dart'; // استيراد hive_error_handler.dart
import 'general_error_handler.dart'; // استيراد general_error_handler.dart
class ErrorManager {
static ErrorHandler getHandler(String type) {
switch (type) {
case 'supabase':
return SupabaseErrorHandler();
case 'firebase':
return FirebaseErrorHandler();
case 'hive':
return HiveErrorHandler();
default:
return GeneralErrorHandler();
}
}
}
نصائح إضافية للاستخدام
- التنظيم في المشروع: قم بإنشاء مجلد مثل lib/error_handling/ وضع هذه الملفات بداخله للحفاظ على ترتيب الكود.
- التكامل: كما في المثال السابق، استخدم ErrorManager.getHandler(‘supabase’) للحصول على الـ Handler المناسب في دوال تطبيقك.
- التوسع: إذا أردت إضافة Handler جديد (مثل لـ SQLite)، أنشئ ملفاً جديداً يرث من ErrorHandler وأضفه إلى ErrorManager.
- الاختبار: يُفضل اختبار كل فئة بشكل منفصل باستخدام أدوات مثل flutter test لضمان تغطية الحالات المختلفة.
هذا التقسيم يجعل الكود سهل الرفع والصيانة على GitHub Gist. إذا كنت بحاجة إلى تعديلات إضافية، فأخبرني.
الخاتمة
يوفر هذا النموذج حلاً قابلاً للتوسع وسهل الاستخدام لإدارة الأخطاء في تطبيقات Flutter. بمجرد نسخ الكلاسات إلى مشروعك، يمكنك توسيعها بإضافة Handlers جديدة. يُنصح باختبار الخدمة باستخدام وحدات الاختبار (Unit Tests) لضمان تغطية جميع الحالات. هذا النهج يعزز جودة التطبيق ويقلل من الوقت المستغرق في الصيانة المستقبلية.

