Files
tripz-llc/docs/23-flutter-conventions.md
T

14 KiB

23 — قانون تطبيقات فلاتر (الراكب والسائق)

هذا المستند مُلزِم. كل كود Dart في apps/rider وapps/driver يتبعه. أي انحراف يُصحَّح لا يُبرَّر. اقرأه كاملاً قبل كتابة أول سطر في أي جلسة جديدة — هو العقد الذي يجعل جلستين مختلفتين تُنتجان نفس البنية.

آخر تحديث: 2026-07-19. ذو صلة: 22 §1.5 (الطبقات) · 19 (الاستحقاقات) · 26 (نظام التصميم الثابت — قانون الشكل المكمّل لهذا القانون) · tripz-mobile-store-identity.


0. المبادئ الخمسة (إن تعارض شيء معها فهي تُرجَّح)

  1. التطبيقان توأمان في البنية، مختلفان في الميزات. نفس المجلدات ونفس التسميات ونفس طبقة الشبكة. من يعرف الراكب يعرف السائق فوراً.
  2. السيرفر هو مصدر الحقيقة. التطبيق يعرض ما يُعطى. لا منطق تسعير ولا حساب عمولة ولا قرار مطابقة في Dart — أبداً.
  3. الحالة صريحة، لا سحر. Cubit افتراضاً، Bloc عند تعدّد الأحداث. لا GetX ولا متغيّرات عامة قابلة للتغيير.
  4. الميزة تُقفل من نقطة واحدة (تسجيل الموديول)، لا بـif مبعثرة في الشاشات.
  5. الأصيل مُعاد استعماله لا مُعاد بناؤه، وهو موحّد عبر كل المستأجرين (شرط Shorebird).

1. بنية المجلدات — مُلزِمة حرفياً

apps/<rider|driver>/lib/
├── main.dart                 # نقطة الدخول: تهيئة ثم runApp — لا منطق
├── app.dart                  # MaterialApp + الثيم + الراوتر + المزوّدون العامّون
├── core/
│   ├── build_config.dart     # ⚠️ مُولَّد — لا يُحرَّر (N3)
│   ├── config.dart           # إعداد وقت التشغيل (--dart-define)
│   ├── di.dart               # get_it: تسجيل التبعيات
│   ├── router.dart           # go_router: كل المسارات
│   ├── design/               # نظام التصميم (docs/26): tokens · typography · tripz_colors · theme
│   ├── ui/                   # عدة المكونات المشتركة (docs/26 §7): TripzScaffold · TripzButton …
│   ├── l10n/                 # ملفات ARB عربي/إنجليزي — كل نص مرئي يمرّ هنا
│   ├── api/
│   │   ├── api_client.dart   # Dio + الاعتراضات (توكن · مستأجر · توقيع · جهاز)
│   │   └── api_exception.dart
│   └── storage/
│       └── token_store.dart  # تخزين آمن للتوكنات
└── features/<feature>/
    ├── cubit/                # <feature>_cubit.dart + <feature>_state.dart
    ├── data/                 # <feature>_repository.dart + models/
    └── view/                 # الشاشات + widgets/

قواعد لا تُخالَف:

  • الميزة لا تستورد من ميزة أخرى. التشارك يصعد إلى core/ أو إلى موديول مشترك. استيراد features/trip/… من داخل features/auth/… ممنوع.
  • view/ لا يستورد data/ مباشرة — يمرّ بالـCubit دائماً.
  • ملف واحد = صنف عام واحد.
  • لا مجلد utils/ عام (مقبرة الشيفرة). الدالة تعيش قرب ما تخدمه.

2. التسمية

العنصر القاعدة مثال
ملفات snake_case.dart trip_repository.dart
أصناف PascalCase TripCubit
الميزة (مجلد) مفرد، إنجليزي features/trip/
الحالة <Feature>State بحقل status TripState
الاستحقاق Features.<camelCase> Features.marketIntel

النصوص المرئية للمستخدم عربية؛ أسماء الرموز والملفات إنجليزية. لا نصّ عربي مكتوب داخل widget مباشرة — يمرّ بطبقة الترجمة.

3. إدارة الحالة

Cubit افتراضاً. Bloc فقط حين تتعدّد الأحداث الواردة على نفس الحالة (مثل شاشة الرحلة الحيّة: سوكت + موقع + إجراء المستخدم معاً).

قواعد:

  • الحالة غير قابلة للتغيير (copyWith)، وتحمل status من enum — لا bool isLoading متناثرة.
  • الـCubit لا يعرف Flutter: لا BuildContext، لا SnackBar، لا تنقّل. يُصدر حالة، والواجهة تتصرّف.
  • الـCubit لا ينادي Dio مباشرة — يمرّ بـRepository دائماً.
  • خطأ الشبكة يتحوّل إلى رسالة عربية داخل الـCubit، والواجهة تعرض state.error كما هو.
// النمط المعتمد
enum TripStatus { idle, loading, active, error }

class TripState {
  final TripStatus status;
  final Trip? trip;
  final String? error;
  const TripState({this.status = TripStatus.idle, this.trip, this.error});
  TripState copyWith({...}) => ...;
}

4. طبقة الشبكة — نقطة واحدة إجبارياً

كل نداء يمرّ بـcore/api/api_client.dart. لا http.get في أي مكان آخر.

الاعتراضات المطلوبة، بهذا الترتيب:

  1. x-tenant-id = BuildConfig.tenantSlug.
  2. Authorization: Bearer من TokenStore.
  3. x-device-id — بصمة الجهاز (يفرضها السيرفر عند AUTH_REQUIRE_DEVICE_BINDING=true، docs/17 D2).
  4. توقيع HMAC للنقاط المالية (docs/17 I6) — يُحسب على الجسم كما يُرسَل بلا إعادة تسلسل، وإلا فشل توقيع سليم.
  5. تجديد التوكن عند 401 مرة واحدة ثم تسجيل خروج — لا حلقة تجديد لانهائية.

⚠️ التوقيع وربط الجهاز مطفآن على السيرفر الآن ويُفعَّلان بعد أن يرسلهما التطبيق. رتّب: أرسل أولاً، ثم فعّل السيرفر.

التخزين: التوكنات في flutter_secure_storage حصراً — لا SharedPreferences للتوكن.

5. أعلام الميزات (القلب — راجع docs/22 §1.5)

// ✅ الصحيح — const، يُحذف الكود من الـbinary
if (Features.marketIntel) { ... }

// ❌ ممنوع — يُبقي كود الميزة في الملف فيمكن استخراجه
if (featureMap['market_intel'] == true) { ... }

التسجيل المشروط هو القاعدة، والفحص المتناثر استثناء:

// core/di.dart — نقطة الإقفال الوحيدة
if (Features.chat) {
  getIt.registerLazySingleton<ChatRepository>(() => ChatRepository(getIt()));
}
// core/router.dart
if (Features.chat) routes.add(GoRoute(path: '/chat', builder: ...));

القاعدة الحاكمة: الفعّال = BuildConfig ∩ المانيفست الريموت ∩ استحقاقات السيرفر. التطبيق لا يمنح صلاحية — يخفي فقط. من فكّك التطبيق وفعّل كل شيء يصطدم بـ403 من FeatureGuard.

6. الخرائط — intaleq_maps حصراً

القرار (المالك، 2026-07-18، مصحَّح): تُستعمل حزمة intaleq_maps (SDK انطلق، إصدار 2.2.0، مبنية على MapLibre GL) — وهي حزمة المالك نفسه ومنشورة على pub.dev.

⚠️ تصحيح قرار سابق: كان مكتوباً «flutter_map + بلاطات انطلق». هذا خطأ: flutter_map وlatlong2 طبقة منافسة تُعرّف LatLng خاصاً بها، فيجتمع في المشروع نوعان بنفس الاسم. intaleq_maps تُصدِّر LatLng وMarker وPolyline وCameraUpdate بنفسها كبديل drop-in لـgoogle_maps_flutter. لا يُضاف flutter_map ولا latlong2 إطلاقاً.

لماذا انطلق لا جوجل (تصحيح فهم شائع): خرائط جوجل تعمل في سوريا كـAPI وكرندر؛ غير العامل جزئياً هو تطبيق جوجل الرسمي — وهذا لا يعنينا لأن التوجيه داخل تطبيقنا. السبب الحقيقي أن بيانات جوجل في المنطقة قديمة (لم تُحدَّث منذ 2011)، بينما انطلق مُحدَّثة وبمستوى تجاري وأدقّ ميدانياً.

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

مفتاح الخريطة يأتي من البيئة/إعداد المستأجر — لا يُكتب في الكود ولا في المستودع.

7. ما يُعاد استعماله من التطبيق القديم — لا يُعاد بناؤه

المكوّن لماذا
android/ · ios/ تحمل هوية النشر والصلاحيات والتوقيع
secure_string_operations · trip_overlay_plugin · bubble-master إضافات أصيلة مكتوبة ومختبَرة
firebase.json · firebase_options.dart · google-services.json مشروع FCM القائم
shorebird.yaml app_id منشور — تغييره يقطع تحديثات OTA عن المستخدمين الحاليين
key/ شهادات توقيع — فقدانها يعني تعذّر تحديث التطبيق المنشور نهائياً
الخطوط (mohanad · josefin · digit) نُسخ (2026-07-19): الخطوط الجديدة الثابتة Inter + IBM Plex Sans Arabic — القرار وأسبابه في 26 §1. الملفات القديمة تبقى حتى يؤكد المالك حذفها

معرّفات المتجر في tripz-mobile-store-identity — تُنسخ ولا تُكتب من الذاكرة. ⚠️ معرّفا السائق متضاربان بين المنصّتين (com.sefer_driver مقابل com.sefer.driver) — يؤكّدهما المالك قبل أي إصدار.

8. الطبقة الأصيلة موحّدة

لا تُفرَّع android/ أو ios/ بين المستأجرين أو الطبقات. المتغيّر الوحيد المسموح آلياً: applicationId · PRODUCT_BUNDLE_IDENTIFIER · android:label · الأيقونات — وكلها يضبطها scripts/generate-tenant-app.sh.

السبب: Shorebird يرقّع Dart فقط. أصيلٌ موحّد ⇒ ترقية الباقة patch فوري. أصيلٌ متفرّع ⇒ كل ترقية إصدار متجر ومراجعة.

9. الحزم المعتمدة

الغرض الحزمة
الحالة flutter_bloc
الحقن get_it
الشبكة dio
التنقّل go_router
الخرائط intaleq_maps (لا flutter_map ولا latlong2 ولا google_maps_flutter)
التخزين الآمن flutter_secure_storage
الزمن الحقيقي socket_io_client
الإشعارات firebase_messaging + flutter_local_notifications

إدخال حزمة خارج هذا الجدول قرارٌ يُوثَّق هنا أولاً. تُثبَّت أحدث الإصدارات المستقرّة، ويُشغَّل flutter pub outdated قبل كل مجموعة عمل.

10. الفروق المشروعة بين التطبيقين

الراكب السائق
الميزة المحورية طلب رحلة وتتبّعها استقبال العروض وتنفيذها
الموقع عند الاستعمال خلفية دائمة + متحكّم مواقع
الأصيل الخاص — overlay العرض · bubble · حيوية الوجه
المال الدفع المحفظة · الرصيد · السحب

متحكّم موقع السائق (نمط سيرو، مثبت عملياً): تسجيل كل ٣ ثوانٍ · رفع دفعات كل دقيقتين · وعيٌ بالبطارية (تخفيف عند ٢٠٪) · سوكت للبثّ الحي ودفعات للحفظ الدائم. لا تُعِد اختراعه.

11. قواعد العمل اليومية

  • لا بناء ولا اختبار على الماك — يُشغّلهما المالك (راجع no-build-or-test-on-mac). الوكيل يكتب فقط.
  • التعليقات تشرح لماذا لا ماذا، وبالعربية.
  • ملف build_config.dart مُولَّد: تعديله يُمحى عند أول توليد.
  • كل ميزة جديدة تبدأ بسؤال: هل لها علم استحقاق؟ إن كانت مدفوعة فالعلم أولاً، ثم الكود.

12. قائمة تحقّق قبل اعتبار ميزة «منجزة»

  • الحالة عبر Cubit/Bloc، غير قابلة للتغيير، بـstatus من enum
  • الـCubit بلا BuildContext وبلا Dio مباشر
  • كل النداءات عبر ApiClient
  • الميزة المدفوعة محروسة بـFeatures.x وبتسجيل مشروط
  • النصوص عربية وعبر طبقة الترجمة
  • لا استيراد متقاطع بين الميزات
  • السيرفر يفرض ما يخفيه التطبيق (لا حماية بالواجهة وحدها)

13. الاختبار على بناء release لا debug ⚠️

حذف كود الميزة المقفلة يحدث في بناء release/AOT فقط. في debug (JIT) يبقى الكود كله موجوداً ولو كان العلم const false.

فالتحقّق من أن نسخة lite لا تحتوي فعلاً على كود النسخ الأعلى لا يصحّ إلا على بناء release — وأي فحص على debug يعطي نتيجة مضلِّلة. القياس المعتمد: بناء release لكل طبقة ومقارنة الحجم + تفتيش الرموز.

14. مؤجَّل بقرار المالك (2026-07-18)

  • Android Auto (موجود في سيرو: MyCarSession · MyCarScreen · MyCarAppService) وCarPlay — عليهما مشاكل ولا أولوية لهما الآن. يُنظر فيهما لاحقاً.