الميزات والمنطق البرمجي Features & Business Logic
استعراض شامل لكل ميزة في التطبيق، من كود الـ Bloc إلى واجهات المستخدم. A comprehensive review of every feature, from Bloc code to UI screens.
تطبيق MRE CashBook بيحفظ بيانات مالية حساسة، عشان كدة كان لازم يكون فيه نظام أمان قوي بيعتمد على بصمة الصيد أو الوجه (Biometrics).
class AuthGuardScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: sl().authenticate(),
builder: (context, snapshot) {
if (snapshot.data == true) return const MainScreen();
return const LockScreen(); // شاشة القفل لو فشل
},
);
}
}
لاحظ إننا استخدمنا الـ LocalAuthService اللي تم حقنه عن طريق الـ DI. دي ميزة إن كل حاجة تكون منفصلة وسهلة الاختبار.
الكتاب هو الحاوية الأساسية لكل المعاملات. الميزة دي بتسمح للمستخدم بإضافة، تعديل، أو حذف الكتب، وكمان تحديد إذا كان الكتاب "داخل في العرض الإجمالي" ولا لأ.
on((event, emit) async {
await repository.insertBook(event.book);
// البلوك هيرد أوتوماتيك لأننا مسجلين في الـ Stream بتاع الداتابيز
});
النقطة السحرية هنا هي الـ Reactivity. إنت مش محتاج تطلب البيانات يدوياً بعد الإضافة، الداتابيز هي اللي بتبعت إشارة لـ GetIt، والبلوك بيستلمها ويحدث الشاشة.
هنا بيحصل الشغل الحقيقي. كل معاملة ليها مبلغ، وصف، تاريخ، وكمان ممكن يكون ليها مرفقات (صور أو فيديوهات).
إحنا بنستخدم AppCustomScrollView عشان نعرض القائمة الطويلة من المعاملات بأداء عالي جداً (Slivers).
ميزة الـ Auth في التطبيق بتعتمد على مكتبة local_auth. إحنا مغلفين المكتبة دي في خدمة اسمها LocalAuthService عشان تكون سهلة الاستخدام.
class LocalAuthService {
final LocalAuthentication _auth = LocalAuthentication();
Future authenticate() async {
final bool canAuthenticateWithBiometrics = await _auth.canCheckBiometrics;
final bool isDeviceSupported = await _auth.isDeviceSupported();
if (!canAuthenticateWithBiometrics || !isDeviceSupported) return false;
return await _auth.authenticate(
localizedReason: 'يرجى تأكيد هويتك للوصول إلى بياناتك المالية',
options: const AuthenticationOptions(
stickyAuth: true,
biometricOnly: true,
),
);
}
}
ده بيخلي عملية التأكد "لزجة". يعني لو المستخدم فتح الـ Recent Apps ورجع للتطبيق تاني، الموبايل بيفضل فاكر إنه كان بيحاول يعمل Auth ومبيقفلش العملية فجأة.
إدارة الكتب بتتم عن طريق BooksBloc. البلوك ده هو اللي بيتحكم في كل "الأرفف" المالية بتاعتك. تعالوا نشوف أهم الـ Events اللي فيه:
بيفتح "ماسورة" (Stream) مع الداتابيز. أي تغيير يحصل في الداتابيز، الشاشة بتتحدث فوراً بدون ما تطلب تاني.
بيتحقق من صحة البيانات (Validation) قبل ما يبعتها للـ Repository.
بيعمل "حذف متسلسل" (Cascade Delete). لما بتمسح كتاب، كل الديون والعمليات اللي جواه بتتمسح معاه أوتوماتيك من الداتابيز عشان ميسيبش garbage.
| Field | Type | Purpose |
|---|---|---|
id | Int | المعرف الفريد للكتاب. |
name | String | اسم الكتاب (مثلاً: حسابات المحل). |
isIncludedInTotal | Bool | هل نجمع فلوس الكتاب ده مع الفلوس الكلية؟ |
كل "كتاب" جواه مئات "المعاملات". الميزة دي هي الأدق في التطبيق لأنها بتتعامل مع أرقام، تواريخ، وملفات.
class EntriesBloc extends Bloc {
final BookDao _bookDao;
final EntryDao _entryDao;
// بنسمع للتغييرات في المعاملات بتاع الكتاب ده بس
void _onLoadEntries(LoadEntries event, Emitter emit) {
_entriesSubscription?.cancel();
_entriesSubscription = _entryDao.watchEntriesWithBook(event.bookId).listen(
(entries) => add(OnEntriesUpdated(entries)),
);
}
}
لاحظ هنا إننا استخدمنا watchEntriesWithBook. ده استعلام SQL معقد (Join) بيجيب بيانات المعاملة ومعاها بيانات الكتاب المرتبط بيها في "خبطة واحدة".
المعاملة ممكن يكون ليها صور أو فيديوهات. إحنا مش بنحفظ الصور جوه الداتابيز (عشان مساحتها متكبرش)، إحنا بنحفظ "المسار" (Path) بتاع الصورة بس.
// إزاي بنعرض الصورة؟
Image.file(File(entry.attachmentPath!));
الـ Dashboard هو مراية المستخدم. هنا بيعرف هو كسبان ولا خسران، وفلوسه بتروح فين بالظبط.
void _calculateStats(List data) {
double totalIncome = 0;
double totalExpense = 0;
for (var item in data) {
if (item.book.isIncludedInTotal) {
totalIncome += item.totalIncome;
totalExpense += item.totalExpense;
}
}
emit(DashboardLoaded(income: totalIncome, expense: totalExpense));
}
إحنا بنستخدم مكتبة fl_chart عشان نحول الأرقام دي لرسوم بيانية (Pie Chart & Bar Chart) تفتح النفس.
الإعدادات هي المكان اللي المستخدم بيطوع فيه التطبيق لمزاجه. إحنا بنستخدم AppSettingsCubit عشان نتحكم في الحاجات دي.
class AppSettingsCubit extends Cubit {
void toggleTheme() {
final newMode = state.themeMode == ThemeMode.light
? ThemeMode.dark
: ThemeMode.light;
emit(state.copyWith(themeMode: newMode));
_saveThemePref(newMode);
}
void changeLanguage(Locale locale) {
emit(state.copyWith(locale: locale));
_saveLocalePref(locale);
}
}
لاحظ إن التغيير بيسمع في التطبيق كله "لحظياً" لأننا بنستخدم BlocBuilder في أعلى نقطة في الـ Widget Tree (ملف main.dart).
إحنا بنعرض سياسة الخصوصية باستخدام webview_flutter. ده بيخلينا نحدث السياسة من غير ما نبعت تحديث جديد للأبلكيشن على الستور.
أقوى ميزة في MRE CashBook هي إنك تقدر تضيف حقول زيادة لأي معاملة (زي: اسم المورد، رقم الفاتورة، إلخ). إحنا معملناش ده بجدول واحد ثابت، إحنا عملنا نظام مرن جداً.
ده جدول فيه أسماء الحقول اللي المستخدم كريتها (مثلاً حقل اسمه "رقم التليفون").
ده جدول تالت بيربط المعاملة (Entry) بالحقل (CustomField) وبيحط فيه القيمة (Value).
// إزاي بنجيب كل الحقول لمعاملة معينة؟
Future> getEntryWithFields(int entryId) {
return (select(entries)..where((t) => t.id.equals(entryId)))
.join([
leftOuterJoin(entryFieldValues, entryFieldValues.entryId.equalsExp(entries.id)),
leftOuterJoin(customFields, customFields.id.equalsExp(entryFieldValues.fieldId)),
]).get();
}
ده بيسمح للمستخدم بتحويل التطبيق لـ CRM صغير أو مخزن بسيط، وكل ده بفضل قوة الـ Joins في SQLite.
لما يكون عندك آلاف المعاملات، البحث بالعين بيكون مستحيل. إحنا عملنا محرك بحث "لحظي" (Debounced Search) بيفهم إنت بتكتب إيه وبيدور في الوصف، رقم المعاملة، وحتى الحقول المخصصة.
// في الـ Bloc، بنستخدم الـ Transformation عشان منعملش Search مع كل حرف
on(
(event, emit) async {
// استعلام SQL بسيط لكن فعال
final results = await repository.searchEntries(event.query);
emit(EntriesLoaded(results));
},
transformer: restartable(), // بيلغي البحث القديم لو كتبت حرف جديد بسرعة
);
استخدام restartable() من مكتبة bloc_concurrency بيوفر كتير في استهلاك البطارية والبروسيسور، لأنه بيوقف أي عملية بحث قديمة ملهاش لازمة.
إيه اللي حصل في التطبيق النهاردة؟ مين مسح الكتاب ده؟ سجل النشاطات هو "الصندوق الأسود" بتاع MRE CashBook.
class ActivityLogs extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get action => text()(); // مثلاً: 'add_entry'
TextColumn get description => text()(); // وصف بالعربي والإنجليزي
DateTimeColumn get createdAt => dateTime()();
}
كل مرة بتعمل فيها عملية (Add, Update, Delete)، إحنا بنرمي "سطر" في الجدول ده. ده مش بس مفيد للمستخدم، ده كمان بيساعدنا في الـ Debugging لو حصلت مشكلة.
| Action | Description (AR) | Description (EN) |
|---|---|---|
book_created | تم إنشاء كتاب جديد | New book created |
entry_deleted | تم حذف معاملة | Entry deleted |
settings_changed | تغيير في الإعدادات | Settings updated |
المستخدم غالباً بيحتاج يطبع كشف حساب أو يبعته للعميل. ميزة الـ PDF في MRE CashBook بتطلع ملفات منظمة، فيها براند التطبيق، وحسابات دقيقة.
class PdfExportService {
Future generateReport(List entries, Book book) async {
final pdf = pw.Document();
pdf.addPage(
pw.Page(
build: (context) => pw.Column(
children: [
_buildHeader(book),
_buildTable(entries),
_buildFooter(), // فيه لينكات المبرمج والتايمستامب
],
),
),
);
return await _savePdf(pdf);
}
}
عشان الجدول يكون سهل في القراءة، بنستخدم تقنية الـ Zebra Striping، وهي إننا نغير لون خلفية السطور بالتناوب (سطر أبيض وسطر رمادي فاتح).
// منطق التلوين
rowDecoration: index % 2 == 0
? pw.BoxDecoration(color: PdfColors.grey100)
: null,
إيه اللي يحصل لو المستخدم غير موبايله؟ إحنا بنسمح للمستخدم "يصدر" (Export) ملف قاعدة البيانات بالكامل ويبعته على الواتساب أو الإيميل.
Future exportDatabase() async {
final dbFile = await DatabaseProvider.getDatabaseFile();
if (await dbFile.exists()) {
await Share.shareXFiles(
[XFile(dbFile.path)],
text: 'MRE CashBook Database Backup',
);
}
}
ملف قاعدة البيانات .sqlite هو أغلى ما يملك المستخدم. دايماً بننصح في شاشة الإعدادات إن المستخدم يعمل Backup دوري عشان لو الموبايل ضاع بيانه متضيعش.
الميزات في MRE CashBook مش شغالة في جزر منعزلة، هي بتكلم بعضها طول الوقت. الرسمة دي بتوضح المسار:
graph TD
A[Auth Feature] -- Success --> B[Main Screen]
B --> C[Books Feature]
C -- Book Selected --> D[Entries Feature]
D -- New Entry --> E[Database]
E -- Stream Update --> F[Dashboard Feature]
E -- Sync --> G[Backup/Export]
D -- Generate --> H[PDF Service]
لاحظ إن الـ Database هي القلب اللي كل الميزات بتسمع منه. ده بيخلي الواجهة دايماً متحدثة (Reactive UI).
أشهر المشاكل اللي ممكن تقابلك وأنت بتطور ميزات جديدة:
المشكلة: ضفت معاملة بس مظهرتش في الـ Dashboard.
الحل: اتأكد إن الـ isIncludedInTotal معمول له true في جدول الكتب. لو الكتاب مستبعد، الـ Dashboard مش هيحسب أرقامه.
المشكلة: البصمة مش بتفتح في الـ Emulator.
الحل: لازم تفعل الـ Biometrics من إعدادات الـ Emulator نفسه (Features -> Fingerprint). في الموبايل الحقيقي، اتأكد إن المستخدم مسجل بصمة أصلاً.
المشكلة: الـ PDF بيطلع صفحات فاضية.
الحل: ده غالباً بسبب حجم الـ Table أكبر من الصفحة. استخدم pw.MultiPage بدل pw.Page عشان يتحمل الزيادة ويبدأ صفحة جديدة أوتوماتيك.
SUM(amount) بتنفذها قاعدة البيانات في ملي ثانية وترجع لنا الرقم النهائي بس.SUM(amount). SQLite is extremely optimized for this, returning results in milliseconds.دي المصطلحات اللي بنستخدمها كفريق تطوير عشان نوصف الميزات:
- Biometrics: التحقق الحيوي عن طريق البصمة أو الوجه.
- Reactivity: قدرة الواجهة على التحديث التلقائي بمجرد تغيير البيانات.
- Cascade Delete: الحذف المتتابع (مثلاً حذف المعاملات عند حذف الكتاب).
- Debounce: تأخير تنفيذ العملية (زي البحث) لغاية ما المستخدم يبطل كتابة.
- Zebra Striping: تلوين سطور الجدول بألوان مختلفة لسهولة القراءة.
- Custom Fields: حقول إإضافية بيعرفها المستخدم حسب حاجته.
- Audit Log: سجل تتبع لكل العمليات اللي حصلت في السيستم.
- Sticky Auth: بقاء عملية التحقق نشطة حتى لو خرجت من التطبيق ورجعت بسرعة.
- Pie Chart: الرسم البياني الدائري اللي بيوضح توزيع المصاريف.
- Export: تصدير البيانات بصيغة
.sqliteأو.pdf. - SQL Join: ربط أكتر من جدول ببعض عشان نجيب بيانات متكاملة.
- PDF Layout: تصميم شكل الملف اللي هيطلع للمطبعة أو للمشاركة.
- Cubit: نسخة خفيفة ومبسطة من الـ Bloc لإدارة الحالة البسيطة.
- Repository: الطبقة اللي بتفصل بين الـ Bloc ومصدر البيانات (DB/API).
- UI Overlay: طبقة فوق الواجهة لعرض تنبيهات أو قوائم اختيار سريعة.
الجدول ده بيلخص كل اللي التطبيق بيقدر يعمله، والملفات المسؤولة عن كل جزء:
| Feature | Purpose (AR) | Core Files | Dependencies |
|---|---|---|---|
| Biometrics | تأمين الدخول بالبصمة. | auth_guard_screen.dart | local_auth |
| Ledger Management | إدارة كتب الحسابات المختلفة. | books_bloc.dart | drift |
| Daily Entries | تسجيل الديون والمدفوعات. | entries_bloc.dart | drift |
| Dashboards | رؤية شاملة للوضع المالي. | dashboard_cubit.dart | fl_chart |
| Media Attachments | إرفاق صور وفيديوهات للعملية. | image_picker_helper.dart | image_picker |
| PDF Export | تصدير كشوفات الحساب. | pdf_export_service.dart | pdf |
| DB Backup | نسخ احتياطي لقاعدة البيانات. | database_provider.dart | share_plus |
| Theming | الوضع الليلي والنهاري. | app_settings_cubit.dart | flutter_bloc |
| Localization | دعم العربية والإنجليزية. | l10n/ | easy_localization |
عشان تبني ميزة صح في MRE CashBook، إحنا بنمشي على خطوات ثابتة بتضمن الجودة:
أول حاجة بنعرف الجدول في الداتابيز. البيانات هي الأساس.
بنكتب الـ Logic اللي هيتعامل مع البيانات دي (Add, Load, Filter).
بنبني الواجهة باستخدام الـ Widgets اللي موجودة في lib/core/widgets عشان نحافظ على الشكل الموحد.
بنضيف الترجمات (AR/EN) ونجرب الميزة على الموبايل والتابلت.
التزامنا بالـ Clean Architecture هو اللي بيخلينا نقدر نضيف ميزة زي "الحقول المخصصة" في يوم واحد من غير ما نهد الكود القديم.
اختبر نفسك وشوف مدى فهمك لأجزاء التطبيق:
transformer: restartable() في البلوك عشان نلغي عمليات البحث القديمة.
[ User Action ] ──▶ [ AddEntry Screen ] ──▶ [ EntriesBloc ]
│
┌─────────────────────────────────────────┘
▼
[ EntryDao (Drift) ] ──▶ [ SQLite DB File ]
│
▼ (Reactive Stream Update)
│
[ DashboardCubit ] ──▶ [ Stats Map ] ──▶ [ FlChart UI ]
التدفق ده بيحصل في أجزاء من الثانية، وده اللي بيدي إحساس الـ Smoothness في التطبيق.
إحنا مش بنوقف تطوير. دي أهم الميزات اللي بنخطط لإضافتها في الإصدارات الجاية:
- Cloud Sync: ربط الحسابات بـ Firebase عشان البيانات تكون متاحة على أكتر من جهاز.
- Dynamic Tax Calculation: حساب الضرائب والقيمة المضافة أوتوماتيك لكل معاملة.
- OCR Scan: تصوير الفاتورة بالموبايل والتطبيق يستخرج منها المبلغ والتاريخ لوحده باستخدام الـ AI.
- CSV/Excel Export: تصدير البيانات بصيغة إكسيل عشان المحاسبين يقدروا يشتغلوا عليها.
عشان تحافظ على نظافة الكود في MRE CashBook، لازم تتبع القواعد دي وأنت بتبني أي ميزة جديدة:
لا تلمس الـ UI قبل الداتابيز: صمم جدولك الأول، هو ده اللي هيحدد شكل المنطق بتاعك.
البلوك هو العقل: أي عملية حسابية أو فلترة لازم تتم جوه الـ Bloc/Cubit، والـ UI بس بيعرض النتيجة.
احترم الـ Extensions: استخدم context.colors و context.tr() دايماً؛ إياك والألوان الثابتة أو النصوص الهارد كود.
التصرف للتابلت: فكر دايماً شكل الميزة هيكون إيه على التابلت قبل ما تبدأ، واستخدم ResponsiveLayout.
سجل كل شيء: ارمي سطر في الـ ActivityLogs لكل عملية مؤثرة المستخدم بيعملها.
نظف خلفك: لو مسحت حاجة، اتأكد إن كل متعلقاتها اتمسحت (Cascade Delete).
الاستجابة اللحظية: ميزة المستخدم مش هيحس بقوتها إلا لو البيانات اتحدثت قدامه من غير ما يعمل ريفريش.
الأمان ليس رفاهية: ميزات الحذف لازم تكون محمية بـ Dialog تأكيدي أو بصمة لو كانت حساسة.
استخدم الـ Core: لا تعيد اختراع العجلة؛ الـ lib/core/widgets فيها 90% من اللي هتحتاجه.
التوثيق بصمت: سمي المتغيرات بأسماء واضحة، ولا تستخدم الكومنتات إلا لشرح الـ "ليه" مش الـ "إيه".
تخيل إنك عاوز تضيف ميزة "الاشتراكات الشهرية" (Subscriptions). فكر في الخطوات دي:
هتحتاج جدول للاشتراكات فيه (الاسم، المبلغ، تاريخ التجديد، والحالة نشط أم لأ).
هتعمل SubscriptionCubit بيسمع للاشتراكات ويقارن تاريخ النهاردة بتاريخ التجديد عشان يبعت تنبيه.
إزاي هتعرض إجمالي مصاريف الاشتراكات في الرسم البياني للـ Dashboard؟
تعال نتبع رحلة "حذف كتاب" من ساعة ما المستخدم يدوس على الزرار لغاية ما يختفي من الشاشة:
المستخدم بيدوس على أيقونة المسح. بنظهر AdaptiveOverlays.showAdaptiveModal للتأكيد.
لو أكد، بنبعت الـ Event: context.read<BooksBloc>().add(DeleteBook(id)).
البلوك بينادي الـ Repository، والـ Repository بيمسح السطر من الداتابيز.
الداتابيز بتبعت إشارة للـ watchBooks Stream إن البيانات اتغيرت.
البلوك بيستلم الـ Stream الجديد ويبعت BooksLoaded، والـ UI بيتحدث بالكتب المتبقية بس.
الجمال هنا إن الـ Bloc مبيحتفظش بحالة "الكتب" عنده يدوياً، هو مجرد "نافذة" بتعرض اللي في الداتابيز لحظة بلحظة.
| Lesson | Key Takeaway (AR) | Technical Tools |
|---|---|---|
| Auth | الأمان يبدأ من بصمة الإصبع. | local_auth |
| Books | الكتب هي الحاوية التنظيمية. | watchStream |
| Entries | المعاملات المالية هي قلب العمل. | Slivers |
| Dashboard | الأرقام لا تكذب، والرسوم توضح. | fl_chart |
| Custom Fields | المرونة هي سر البقاء والاستمرار. | SQLite Joins |
| Search | البحث اللحظي يوفر الوقت والجهد. | restartable() |
| Activity Logs | الشفافية تمنع الأخطاء البشرية. | DB Audit |
| PDF Export | الاحترافية تظهر في التقارير. | pdf package |
| Backups | بياناتك أمانة، فاحمِها بالنسخ. | share_plus |
| Architecture | نظام الميزات المنفصل يسهل التطوير. | Clean Architecture |
قبل ما تقول إن الميزة دي "خيرة"، اتأكد إنك عملت الـ 15 حاجة دول:
| Checklist Item | AR Description | Done? |
|---|---|---|
| DB Schema Defined | الجدول متعرف صح في Drift. | ✅ |
| Sync logic (Stream) | الـ UI بيحدث لحظياً. | ✅ |
| Error Handling | فيه Try/Catch ورسايل خطأ واضحة. | ✅ |
| Biometric Guard | لو الميزة حساسة، عليها قفل. | ✅ |
| Localization (AR) | كل النصوص مترجمة عربي. | ✅ |
| Localization (EN) | كل النصوص مترجمة إنجليزي. | ✅ |
| Dark Mode Support | الألوان واضحة في الوضع الليلي. | ✅ |
| Tablet Optimization | الشكل مظبوط على الشاشات الكبيرة. | ✅ |
| Media Support | المرفقات شغالة (صور/فيديو). | ✅ |
| PDF Integration | الميزة بتظهر في التقارير. | ✅ |
| Searchable | ممكن نوصل للبيانات بالبحث. | ✅ |
| Audit Logged | العملية متسجلة في الـ History. | ✅ |
| Input Validation | المستخدم مبيعرفش يدخل بيانات غلط. | ✅ |
| Performance Check | الميزة مش بتقل التطبيق. | ✅ |
| Clean Code Principles | الكود منظم ومكتوب صح. | ✅ |
الفصل ده كان الأطول والأهم في كورس MRE CashBook. لأن الميزات هي اللي بتقدم القيمة للمستخدم في الآخر. لما بتنفذ ميزة بنظام الـ Clean Architecture وتراعي فيها الأداء والأمان، إنت مش بس بتبني تطبيق، إنت بتبني "أداة" المستخدم يقدر يعتمد عليها في حياته اليومية.
في الفصل الجاي: هننقل من "المنطق" لـ "الجمال". هنعرف إزاي عملنا نظام الثيمات (UI Theming) اللي بيخلي التطبيق مريح للعين في كل الأوقات.
الميزات هي قلب التطبيق. تنظيمها في فولدرات منفصلة (Clean Architecture) هو اللي بيخليه يعيش سنين بدون مشاكل.