بنية التطبيق وأنماط التصميم

التواصل بين الميزات والوحدة الجوهرية

16 دقيقة الدرس 11 من 12

التواصل بين الميزات والوحدة الجوهرية

مع نمو تطبيق Flutter، من الطبيعي تقسيمه إلى وحدات ميزات — auth، وprofile، وnotifications، وpayments، وما إلى ذلك. التحدي المعماري الحرج هو: كيف يمكن لميزة A أن تُطلق سلوكاً في ميزة B دون استيراد ميزة B مباشرةً؟ الاستيرادات المباشرة تُنشئ ترابطاً محكماً، وتُصعّب اختبار الوحدات، وتحوّل عمليات إعادة الهيكلة إلى كوارث متتالية. الحل هو الوحدة الجوهرية (core module) التي تعمل كطبقة عقود مشتركة بين الميزات.

لماذا الاستيرادات المباشرة بين الميزات خطيرة

تخيّل تنفيذاً ساذجاً حيث تستورد ميزة auth ميزة home للتنقل بعد تسجيل الدخول:

// سيئ: auth/login_page.dart تستورد home مباشرة
import 'package:my_app/features/home/home_page.dart'; // ترابط محكم!

void _onLoginSuccess() {
  Navigator.of(context).push(
    MaterialPageRoute(builder: (_) => const HomePage()),
  );
}

هذا الاستيراد الواحد يعني:

  • وحدة auth تعتمد الآن على وحدة home بالكامل
  • اختبار وحدة auth يتطلب إنشاء مثيلات من ودجات home
  • إعادة تسمية أو إزالة HomePage يكسر وحدة auth
  • يمكن أن تتشكل تبعية دائرية إذا استوردت home يوماً ما auth
ملاحظة: الهدف هو أن يعتمد كل مجلد ميزة فقط على وحدة core، وأبداً على الميزات الأخرى المجاورة. يجب أن يكون رسم بياني التبعية شجرةً لا شبكةً.

الوحدة الجوهرية كطبقة عقود مشتركة

الوحدة الجوهرية (التي تسمى أحياناً shared أو common) هي حزمة Dart من الدرجة الأولى أو مجلد يُسمح لكل ميزة باستيراده. تكشف عن:

  • واجهات مجردة (فئات abstract) — عقود يجب على الميزات تنفيذها
  • نماذج النطاق / الكيانات — فئات بيانات نقية مشتركة عبر الميزات
  • عقود التنقل — موجّه مجرد أو ثوابت أسماء المسارات
  • ناقل الأحداث / قنوات البث — تعريفات أحداث عبر الميزات ذات أنواع محددة
  • أدوات مساعدة — تنسيق، إضافات (extensions)، رموز السمات
// core/navigation/app_router.dart
abstract class AppRouter {
  void goToHome();
  void goToProfile(String userId);
  void goToLogin();
  void pop();
}

// core/events/app_event.dart
abstract class AppEvent {
  const AppEvent();
}

class UserLoggedIn extends AppEvent {
  final String userId;
  const UserLoggedIn(this.userId);
}

class UserLoggedOut extends AppEvent {
  const UserLoggedOut();
}

مع وجود هذا العقد، لا تستورد ميزة auth أبداً إلا من core. يعيش تنفيذ الموجّه الملموس في main.dart أو طبقة app، التي تربط كل شيء معاً عبر حقن التبعية.

التنقل بين الميزات عبر موجّه مجرد

أدخِل AppRouter المجرد في كل ميزة. تستدعي الميزة الأساليب على الواجهة؛ ولا تعرف أبداً أي تنفيذ ملموس يعمل تحتها:

// features/auth/presentation/login_notifier.dart
import 'package:my_app/core/navigation/app_router.dart';
import 'package:my_app/core/events/app_event.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class LoginNotifier extends StateNotifier<AsyncValue<void>> {
  LoginNotifier(this._router, this._eventBus)
      : super(const AsyncValue.data(null));

  final AppRouter _router;
  final StreamController<AppEvent> _eventBus;

  Future<void> login(String email, String password) async {
    state = const AsyncValue.loading();
    try {
      // ... استدعاء مستودع المصادقة ...
      const userId = 'user-123';
      _eventBus.add(const UserLoggedIn(userId)); // بث الحدث
      _router.goToHome();                        // التنقل بدون استيراد home
      state = const AsyncValue.data(null);
    } catch (e, st) {
      state = AsyncValue.error(e, st);
    }
  }
}
نصيحة: استخدام StreamController<AppEvent> (أو حزمة مثل event_bus) كناقل للأحداث يعني أن أي ميزة يمكنها الاستماع لـ UserLoggedIn دون معرفة أن وحدة auth كانت المُرسِل. المعرفة المشتركة الوحيدة هي فئة الحدث نفسها — التي تعيش في core.

بث الأحداث بدون تبعيات مباشرة

نمط ناقل الأحداث يفصل المُرسِل عن كل المستقبِلين. يمكن لميزة الإشعارات الاستجابة لـ UserLoggedIn لتحميل عدد إشعارات المستخدم؛ يمكن لميزة الملف الشخصي الاستجابة لجلب الملف — لا أيٌّ منهما يستورد auth:

// features/notifications/application/notifications_service.dart
import 'package:my_app/core/events/app_event.dart';

class NotificationsService {
  NotificationsService(Stream<AppEvent> events) {
    events.whereType<UserLoggedIn>().listen((event) {
      _loadNotifications(event.userId);
    });
    events.whereType<UserLoggedOut>().listen((_) {
      _clearNotifications();
    });
  }

  void _loadNotifications(String userId) {
    // جلب من API — لا حاجة لاستيراد auth
  }

  void _clearNotifications() {
    // إعادة تعيين الحالة المحلية
  }
}

ربط الميزات معاً في طبقة التطبيق

main.dart أو وحدة AppModule المخصصة هي المكان الوحيد الذي يعرف عن كل ميزة. يُنشئ تنفيذات ملموسة ويحقنها:

// app/app_module.dart  (جذر التركيب)
import 'package:my_app/core/navigation/app_router.dart';
import 'package:my_app/core/events/app_event.dart';
import 'package:my_app/app/go_router_impl.dart';       // الموجّه الملموس
import 'package:my_app/features/notifications/...';    // الخدمة الملموسة

final _eventBus = StreamController<AppEvent>.broadcast();

final appRouterProvider = Provider<AppRouter>(
  (ref) => GoRouterImpl(ref),
);

final eventBusProvider = Provider<StreamController<AppEvent>>(
  (ref) => _eventBus,
);
تحذير: لا تضع أبداً تنفيذ الموجّه الملموس أو ناقل الأحداث داخل مجلد ميزة. هذا سيُعيد إدخال الترابط. احتفظ بكل الربط في طبقة app — الطبقة الوحيدة التي يُسمح لها بمعرفة كل ميزة.

ملخص

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