Deep Dive

Advanced Flutter,
Dart & design patterns

The layer past the tutorials — language features that move bugs to compile time, the framework internals behind a smooth frame, and the patterns I actually reach for in production apps.

techniques
7techniques
topics
1topics
language level
Dart 3language level

Browse by topic

Jump straight to a technique
Advertisement

Dart Advanced

7

مفاهيم متقدمة في لغة Dart ونظام الـ Type System للوصول لمستوى Senior.

01DartAdvanced

Type Variance: Covariance & Contravariance

مفهوم الـ Type System: متى وكيف نستخدم Covariance و Contravariance في Dart.

The problem

في Dart، لما تحاول تعمل Override لدالة وتغير نوع الـ Parameter لنوع أكثر تحديداً (مثلاً من Animal إلى Dog)، الـ Compiler بيطلع Error لأن باراميترات الدوال contravariant بطبيعتها، ومينفعش الـ Subclass يضيّق النوع مباشرة.

The approach

1. Covariance: بنستخدم كلمة `covariant` في كلاس الـ Parent أو الـ Child عشان نعرّف Dart إن النوع هيتغير بس لازم يكون نوع وارث من الـ Parent. 2. Contravariance: بيسمح لك باستخدام دالة عامة (General) في سياق خاص (Specific Context) — مثل سنتر بيرد على المشاكل العامة يقدر يستقبل ويرد على حالات الطوارئ، بس مش العكس.

dart
abstract class Animal {
  final String name;
  const Animal(this.name);
}

class Dog extends Animal {
  const Dog(super.name);
  void bark() => print('Woof!');
}

// 1. Covariance: استخدام نوع أكثر تحديداً بدلاً من نوع عام
abstract class AnimalTrainer {
  // استخدام covariant يخبر Dart بالسماح بنوع فرعي وارث من Animal
  void train(covariant Animal animal);
}

class DogTrainer extends AnimalTrainer {
  @override
  void train(Dog animal) {
    animal.bark(); // هنا نقدر نستخدم دوال الـ Dog بأمان
  }
}

// 2. Contravariance: استخدام دالة عامة في سياق خاص
typedef DogHandler = void Function(Dog dog);

void processDogEvent(Dog dog, DogHandler handler) {
  handler(dog);
}

// دالة عامة تتعامل مع أي Animal
void generalAnimalHandler(Animal animal) {
  print('Handling animal: ${animal.name}');
}

void main() {
  final dog = Dog('Buddy');
  
  // Contravariance: الدالة العامة (Animal -> void) 
  // مقبولة وتعمل بنجاح في مكان الدالة الخاصة (Dog -> void)
  processDogEvent(dog, generalAnimalHandler);
}

Reach for it when

  • استخدام `covariant` لما كلاس فرعي يحتاج يتعامل حصرياً مع نوع فرعي محدد (مثل DogTrainer يتعامل فقط مع Dog)
  • تمرير General Callbacks لمعالجة Specialized Events (Contravariance)
  • فهم هيكلة Flutter الداخلية مثل تحديثات الـ RenderObject والـ Widgets

Think twice when

  • !لو تقدر تستخدم Generics (`Trainer<T extends Animal>`) لأنها أفضل وأكثر أماناً في وقت الـ Compile time
  • !تضييق نوع الـ Parameter مع وجود احتمالية لتمرير نوع فرعي آخر في الـ Runtime

Gotcha: الأفضل دائماً استخدام الـ Generics بدلاً من `covariant` لأن الـ Generics بتضمن الـ Type Safety في وقت الـ Compile time بدون الحاجة لـ Runtime checks.

02DartIntermediate

Generic Constraints (Bounded Generics)

تقييد الأنواع العامة (Generics) لضمان الأمان والوصول للخصائص المطلوبة وقت الـ Compile time.

The problem

لو كتبت `class Box<T>` فقط بدون قيود، الـ `T` ممكن تكون أي نوع على الإطلاق، وبالتالي لا يمكنك استدعاء أي ميثود خاصة بـ `Animal` داخل الكلاس لأن الـ Compiler لا يضمن نوع `T`.

The approach

بنستخدم `class Box<T extends Animal>` لتقييد الـ Type Parameter بحيث يقبل فقط كلاس `Animal` أو أي كلاس وارث منه. ده بيسمح لك بالوصول لكل دوال وخصائص `Animal` بأمان تام وبدون Type Casting.

dart
abstract class Animal {
  final String name;
  const Animal(this.name);
  void makeSound();
}

class Dog extends Animal {
  const Dog(super.name);
  @override
  void makeSound() => print('Woof!');
}

class Cat extends Animal {
  const Cat(super.name);
  @override
  void makeSound() => print('Meow!');
}

// T extends Animal: يقبل Animal أو أي نوع وارث منه فقط
class Box<T extends Animal> {
  final T item;
  const Box(this.item);

  void inspect() {
    print('Box contains: ${item.name}');
    item.makeSound(); // استدعاء آمن تماماً وقت الـ Compile time
  }
}

void main() {
  final dogBox = Box(Dog('Buddy')); // صحيح
  dogBox.inspect();

  final catBox = Box(Cat('Milo'));  // صحيح
  catBox.inspect();

  // Box<String>('Text'); // Compile Error: String does not extend Animal
}

Reach for it when

  • بناء كلاسات أو دوال عامة تتعامل مع دوال وخصائص محددة لكلاس أساسي
  • كتابة Repositories أو State Holders مشتركة لعائلات معينة من البيانات

Think twice when

  • !لو الكلاس أو الدالة عامة تماماً ولا تعتمد على أي خصائص من كلاس معين (مثل `List<T>`)

Gotcha: يمكن أيضاً استخدام القيود على مستوى الدوال فقط: `void process<T extends Animal>(T item)`.

03DartAdvanced

Mixins, 'on' Keyword & The Diamond Problem

دمج السلوكيات وحل معضلة الوراثة المتعددة عبر التسلسل الخطي (Linearization).

The problem

في لغة Dart، لا يمكن استخدام `extends` لأكثر من كلاس واحد لتجنب معضلة الـ Diamond Problem التي تحدث عند وراثة دالة بنفس الاسم من كلاسين مختلفين.

The approach

1. الـ Mixin: يتيح دمج وإعادة استخدام الكود عبر أكثر من كلاس (`class User with Logger, Cache`). 2. كلمة `on`: تحدد أن الـ Mixin لا يمكن استخدامه (`with`) إلا من كلاسات وارثة من نوع محدد (`mixin Runner on Animal`). 3. حل الـ Diamond Problem: تعتمد Dart على الـ Linearization (التسلسل الهرمي من الشمال لليمين) بحيث يقوم الـ Mixin الأخير بعمل override لأي ميثود قبله بدون أي تضارب.

dart
abstract class Animal {
  final String name;
  const Animal(this.name);
}

// Runner on Animal: الـ Mixin متاح فقط للكلاسات الوارثة من Animal
mixin Runner on Animal {
  void run() => print('$name is running!');
}

mixin Logger {
  void log(String msg) => print('[LOG]: $msg');
}

// دمج أكثر من مكسن
class Dog extends Animal with Runner, Logger {
  Dog(super.name);
}

// حل مشكلة Diamond Problem عبر الـ Linearization
mixin A {
  void execute() => print('Executed from A');
}

mixin B {
  void execute() => print('Executed from B');
}

// في الـ Linearization: B تأتي بعد A لذا فإن B هي التي تنفذ
class Service with A, B {}

void main() {
  final dog = Dog('Rex');
  dog.run(); // من Runner
  dog.log('Online'); // من Logger

  final service = Service();
  service.execute(); // يطبع: "Executed from B" (Linearization)
}

Reach for it when

  • مشاركة سلوكيات مشتركة (مثل Logging, Cache, Serialization) بين كلاسات غير مرتبطة بشجرة وراثة واحدة
  • تقييد تطبيق الـ Mixin على كلاسات معينة فقط باستخدام `on`

Think twice when

  • !لو الكلاس يحتاج إلى State معقدة ومُنشئات (Constructors) خاصة به — الـ Mixin لا يدعم الـ Constructors

Gotcha: قاعدة الـ Linearization: في `class C with A, B`، الـ Mixin المكتوب أقصى اليمين (B) يملك الأولوية القصوى ويعمل Override لنفس الدوال في (A).

04DartExpert

Extensions & Extension Types (Zero-Cost Type Safety)

إضافة دوال جديدة والتمييز بين المعرفات بأمان تام وبدون أي استهلاك للذاكرة (Zero-Cost).

The problem

عند استخدام `String userId` و `String productId`، يمكن بالخطأ كتابة `deleteUser(productId)` وسيعمل الكود بدون خطأ لأن الاثنين `String`. إنشاء كلاس عادي لكل نوع يحل المشكلة لكنه يستهلك Memory وينشئ Objects إضافية.

The approach

1. الـ Extension: يتيح إضافة Features ودوال جديدة للكلاسات بدون تعديل الكود الأصلي. 2. الـ Extension Type: غلاف وقت الـ Compile time فقط. يعتبر `UserId` و `ProductId` نوعين مختلفين تماماً يمنعان الخطأ، بينما في وقت التشغيل (Runtime) هو مجرد `String` عادي بدون أي Object Allocation.

dart
// 1. Extension: إضافة دالة جديدة للـ String بدون تعديل الكود الأساسي
extension StringFormatting on String {
  String capitalize() =>
      isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}';
}

// 2. Extension Type: Zero-Cost Type Wrapper
extension type const UserId(String id) {
  bool get isValid => id.isNotEmpty;
}

extension type const ProductId(String id) {}

void deleteUser(UserId userId) {
  print('User deleted: ${userId.id}');
}

void main() {
  print('flutter'.capitalize()); // يطبع: Flutter

  final user = UserId('usr_101');
  final product = ProductId('prod_909');

  deleteUser(user); // يعمل بنجاح

  // deleteUser(product);
  // Error: The argument type 'ProductId' can't be assigned to parameter 'UserId'
  // حماية كاملة وقت الـ Compile time بدون أي تكلفة Object في الـ Memory!
}

Reach for it when

  • منع أخطاء تبديل المعرفات والـ IDs الحساسة (UserId, OrderId, ProductId)
  • إضافة دوال ومساعدات (Helper methods) للأنواع الجاهزة
  • المسارات الحساسة للأداء (Hot paths) التي تتطلب أقصى سرعة وأقل استهلاك للذاكرة

Think twice when

  • !لو كنت محتاج تفحص النوع في الـ Runtime باستخدام `is` (لأن الـ Extension Type يُمسح في الـ Runtime ويعود لنوعه الأساسي)

Gotcha: الـ Extension Type يمنحك قوة الـ Value Objects والـ Strong Typing بدون أي كلفة تشغيلية على الذاكرة أو أداء التطبيق.

05DartAdvanced

Isolates & Concurrency: Isolates vs Threads

تشغيل العمليات الثقيلة بالتوازي في بيئة معزولة تماماً بدون مشاكل الـ Shared Memory.

The problem

في بيئات الـ Multi-threading التقليدية، مشاركة الذاكرة (Shared Memory) تسبب مشاكل معقدة مثل Race Conditions، Deadlocks، و Data Races، والحاجة لإدارة معقدة للـ Locks و Mutexes. كما أن تشغيل العمليات الحسابية والـ Parsing الثقيل على الـ Main Isolate يجمد الـ UI ويسقط الـ FPS.

The approach

1. الـ Isolate يختلف عن الـ Thread: كل Isolate يمتلك ذاكرة مستقلة تماماً (Own Memory, Heap) و Event Loop خاص به. 2. منع مشاكل الذاكرة المشتركة: عدم مشاركة الذاكرة يقضي تماماً على مشاكل الـ Race Conditions والـ Deadlocks. 3. التواصل عبر الرسائل (Message Passing): باستخدام `SendPort` و `ReceivePort` مع إمكانية الـ Two-way communication، أو استخدام `Isolate.run()` للعمليات المباشرة والسريعة.

dart
import 'dart:isolate';
import 'dart:convert';

// 1. استخدام Isolate.run للعمليات السريعة والمباشرة
Future<List<String>> parseCatalog(String rawJson) async {
  return await Isolate.run(() {
    final List<dynamic> list = jsonDecode(rawJson);
    return list.map((e) => e.toString()).toList();
  });
}

// 2. التواصل ثنائي الاتجاه (Two-Way Communication عبر Ports)
void workerTask(SendPort mainSendPort) {
  final workerReceivePort = ReceivePort();
  // إرسال الـ Port الخاص بالـ Worker للـ Main Isolate
  mainSendPort.send(workerReceivePort.sendPort);

  workerReceivePort.listen((message) {
    print('Worker received: $message');
    // إرسال النتيجة بعد المعالجة
    mainSendPort.send('Processed: $message');
  });
}

void main() async {
  // استقبال أول رسالة تحتوي على SendPort الخاص بالـ Worker
  final mainReceivePort = ReceivePort();
  await Isolate.spawn(workerTask, mainReceivePort.sendPort);

  final workerSendPort = await mainReceivePort.first as SendPort;
  workerSendPort.send('Heavy Calculation Job');
}

Reach for it when

  • معالجة وتحليل ملفات JSON الضخمة جداً (Heavy Parsing)
  • معالجة وضغط الصور والملفات الصوتية والتشفير (Encryption/Decryption)
  • العمليات الحسابية والخوارزميات المعقدة لحماية سلاسة الـ UI من التجمد

Think twice when

  • !عمليات الشبكة والـ I/O العادية (لأن `async/await` غير حاجب للـ Event loop أصلاً)
  • !العمليات الخفيفة جداً حيث تكون كلفة نقل البيانات عبر المنافذ أعلى من وقت المعالجة

Gotcha: تذكر دائماً: `async != Parallelism`! استخدام `async` لا ينقل الكود إلى Thread آخر، بينما الـ `Isolate` هو الوسيلة الوحيدة لتحقيق التوازي الفعلي (Real Parallelism).

06DartIntermediate

Streams: Reactive Events (Single vs Broadcast)

تدفق البيانات غير المتزامن عبر الزمن والفرق بين Single Subscription و Broadcast.

The problem

الـ `Future` يُرجع قيمة واحدة فقط لمرة واحدة (مثل جلب بيانات API منفرد)، وبالتالي لا يناسب التعامل مع مصادر البيانات المستمرة التي تُرسل أحداثاً متتالية عبر الزمن (مثل محادثات الشات، إشعارات الـ WebSockets، أو تغيرات الموقع).

The approach

1. الـ Stream: يمثل تدفقاً مستمراً للأحداث عبر الزمن. 2. الاستماع للـ Stream: يتم عبر `listen()` مع معالجة البيانات، الأخطاء (`onError`)، ونهاية التدفق (`onDone`). 3. Single-Subscription: يستقبل مستمعاً واحداً فقط (مثل قراءة ملف). 4. Broadcast: يستقبل أكثر من مستمع في نفس الوقت (مثل البث التلفزيوني لعدة مشاهدين). 5. التوليد عبر `async*` و `yield`: لإنشاء Stream يرسل البيانات تدريجياً.

dart
import 'dart:async';

class Message {
  final String text;
  final DateTime timestamp;
  Message(this.text) : timestamp = DateTime.now();
}

// 1. توليد Stream باستخدام async* و yield
Stream<Message> messageStream() async* {
  for (int i = 1; i <= 3; i++) {
    await Future.delayed(const Duration(seconds: 1));
    yield Message('Message #$i');
  }
}

void main() async {
  // 2. Single Subscription Stream (مستمع واحد)
  final singleStream = messageStream();
  
  singleStream.listen(
    (message) => print('Single: ${message.text} at ${message.timestamp}'),
    onError: (error) => print('Error: $error'),
    onDone: () => print('Stream completed!'),
  );

  // 3. Broadcast Stream (أكثر من مستمع)
  final controller = StreamController<int>.broadcast();

  // المشاهد الأول
  controller.stream.listen((val) => print('Viewer 1: $val'));

  // المشاهد الثاني
  controller.stream.listen((val) => print('Viewer 2: $val'));

  controller.add(100);
  controller.add(200);
  await controller.close();
}

Reach for it when

  • استقبال الأحداث المستمرة مثل Real-time Chat, WebSockets, Sensors, و Firebase Streams
  • بناء معماريات الـ BLoC والـ Reactive State Management
  • مراقبة حالة الاتصال بالإنترنت وتغيرات الموقع الجغرافي

Think twice when

  • !العمليات التي ترجع قيمة واحدة فقط لمرة واحدة (استخدم `Future` بدلاً منها)

Gotcha: احرص دائماً على عمل `cancel()` للـ `StreamSubscription` أو `close()` للـ `StreamController` لتجنب حدوث تسريب في الذاكرة (Memory Leaks).

07DartAdvanced

Generators: sync* vs async* (yield, yield* & Lazy Streams)

إنتاج القيم بالتسلسل (Lazy Evaluation) والفرق بين Iterable المتزامن و Stream غير المتزامن عبر yield و yield*.

The problem

الدوال التقليدية تقوم بحساب وتجهيز كل القيم دفعة واحدة في الذاكرة كـ `List`، مما يستهلك الذاكرة ولا يدعم الـ Lazy Evaluation. كما أن تجميع وتمرير البيانات المتدفقة مع الوقت أو دمج مصادر بيانات متعددة (مثل Cache + API) بالطرق العادية يتطلب إدارة يدوية معقدة للـ Controllers والـ Subscriptions.

The approach

1. الـ Generator: دالة تنتج القيم قيمة بقيمة (Lazy) عند طلبها من الـ Consumer بدلاً من تجهيز كل شيء دفعة واحدة. 2. `sync*` مع `Iterable<T>`: لإنتاج القيم بالتسلسل بشكل متزامن (Synchronous)، وتُستهلك عبر `for-in` أو `.toList()`. 3. `async*` مع `Stream<T>`: لإنتاج الأحداث غير المتزامنة عبر الزمن (Asynchronous Events)، وتُستهلك عبر `await for` أو `.listen()`. 4. `yield` مقابل `yield*`: - `yield value`: يُخرج قيمة واحدة فقط. - `yield* sequence`: يفوض ويمرر sequence كاملة (سواء Iterable أو Stream آخر مثل دمج Cache ثم API).

dart
import 'dart:async';

// 1. sync* Generator (Lazy Synchronous Iterable)
Iterable<int> countUpTo(int max) sync* {
  for (int i = 1; i <= max; i++) {
    print('Generating synchronous value: $i');
    yield i; // يُنتج القيمة فقط عند طلب الـ Consumer للـ next
  }
}

// 2. async* Generator (Asynchronous Event Stream)
Stream<int> fetchPages() async* {
  for (int page = 1; page <= 3; page++) {
    await Future.delayed(const Duration(milliseconds: 500)); // محاكاة جلب API
    yield page; // ينتج حدث جديد في الـ Stream
  }
}

// 3. yield* Delegation (دمج تسلسل Cache ثم API)
Stream<String> getUserFeed() async* {
  yield* getCachedFeed(); // تفويض أحداث الـ Cache أولاً
  yield* getRemoteFeed(); // ثم تفويض أحداث الـ API
}

Stream<String> getCachedFeed() async* {
  yield 'Cache Item 1';
}

Stream<String> getRemoteFeed() async* {
  await Future.delayed(const Duration(milliseconds: 300));
  yield 'Live Item 2';
}

void main() async {
  // استهلاك sync* عبر for-in (يبدأ التوليد فعلياً مع بداية الـ Loop فقط)
  print('--- sync* Iterable ---');
  for (final num in countUpTo(2)) {
    print('Consumed: $num');
  }

  // استهلاك async* عبر await for
  print('\n--- async* Stream ---');
  await for (final page in fetchPages()) {
    print('Loaded page: $page');
  }

  // استهلاك الـ Delegation عبر yield*
  print('\n--- yield* Delegation ---');
  await for (final feed in getUserFeed()) {
    print('Feed: $feed');
  }
}

Reach for it when

  • بناء سلاسل بيانات ضخمة أو لا نهائية (Infinite / Lazy Sequences) دون حجز الذاكرة مسبقاً
  • معمارية دمج مصادر البيانات (Local Cache أولاً ثم Remote Network) في Stream موحد
  • توليد البيانات المجزأة (Paginated data) تدريجياً مع استجابة المستخدم

Think twice when

  • !لو كانت كل البيانات متوفرة ومجهزة مسبقاً في قائمة صغيرة وثابتة (استخدم `List` مباشرة)
  • !لو كنت محتاج وصول عشوائي مباشر للعناصر بالفهرس (Random Access by Index)

Gotcha: المعادلة الذهبية 🧠: `yield` = أخرج عنصراً واحداً | `yield*` = مرّر وفوض sequence كاملة (Iterable أو Stream).

Want a second pair of eyes on your architecture?

I do code reviews, state-management refactors, and performance work on production Flutter apps.