التعمُّق في أنماط لعبة Dart وسجلاتها

1. مقدمة

تضيف Dart 3 الأنماط إلى اللغة، وهي فئة جديدة رئيسية من قواعد اللغة. بالإضافة إلى هذه الطريقة الجديدة لكتابة رمز Dart البرمجي، هناك العديد من التحسينات الأخرى على اللغة، بما في ذلك:

  • السجلات لتجميع البيانات من أنواع مختلفة
  • معدّلات الفئات للتحكّم في إمكانية الوصول
  • تعبيرات switch وعبارات if-case جديدة

توسّع هذه الميزات الخيارات المتاحة لك عند كتابة رمز Dart البرمجي. في هذا الدرس التطبيقي حول الترميز، ستتعرّف على كيفية استخدامها لجعل الرمز البرمجي أكثر إيجازًا وسلاسة ومرونة.

يفترض هذا الدرس العملي أن لديك بعض المعرفة بلغتَي Flutter وDart. إذا كنت بحاجة إلى مراجعة بعض المعلومات، يمكنك الاستعانة بالمراجع التالية:

ما ستنشئه

ينشئ هذا الدرس التطبيقي حول الترميز تطبيقًا يعرض مستند JSON في Flutter. يحاكي التطبيق ملف JSON واردًا من مصدر خارجي. يحتوي ملف JSON على بيانات المستند، مثل تاريخ التعديل والعنوان والعناوين الرئيسية والفقرات. تكتب الرمز البرمجي لتعبئة البيانات بشكل منظَّم في السجلات حتى يمكن نقلها وفك حزمها أينما تحتاج إليها عناصر واجهة مستخدم Flutter.

بعد ذلك، يمكنك استخدام الأنماط لإنشاء الأداة المناسبة عندما تتطابق القيمة مع هذا النمط. يمكنك أيضًا الاطّلاع على كيفية استخدام الأنماط لتقسيم البيانات إلى متغيرات محلية.

التطبيق النهائي الذي ستنشئه في هذا الدرس التطبيقي حول الترميز هو مستند يتضمّن عنوانًا وتاريخ آخر تعديل ورؤوسًا وفقرات.

ما ستتعلمه

  • كيفية إنشاء سجلّ يخزّن قيمًا متعددة بأنواع مختلفة
  • كيفية عرض قيم متعدّدة من دالة باستخدام سجلّ
  • كيفية استخدام الأنماط لمطابقة البيانات والتحقّق من صحتها وتفكيكها من السجلات والكائنات الأخرى
  • كيفية ربط القيم المطابقة للنمط بمتغيرات جديدة أو حالية
  • كيفية استخدام إمكانات جديدة في عبارة switch، وتعبيرات switch، وعبارات if-case
  • كيفية الاستفادة من التحقّق من الشمولية لضمان التعامل مع كل حالة في عبارة switch أو تعبير switch

2. إعداد البيئة

  1. ثبِّت حزمة تطوير البرامج (SDK) من Flutter.
  2. إعداد محرِّر، مثل Visual Studio Code (VS Code)
  3. اتّبِع خطوات إعداد النظام الأساسي لمنصة مستهدفة واحدة على الأقل (iOS أو Android أو الكمبيوتر أو متصفّح ويب).

3- إنشاء المشروع

قبل التعرّف على الأنماط والسجلات والميزات الجديدة الأخرى، ننصحك بإنشاء مشروع Flutter تكتب فيه كل الرموز البرمجية.

إنشاء مشروع Flutter

  1. استخدِم الأمر flutter create لإنشاء مشروع جديد باسم patterns_codelab. يمنع العلامة --empty إنشاء تطبيق العداد العادي في ملف lib/main.dart، والذي عليك إزالته على أي حال.
flutter create --empty patterns_codelab
  1. بعد ذلك، افتح الدليل patterns_codelab باستخدام VS Code.
code patterns_codelab

عرض المشروع الذي تم إنشاؤه في VS Code

ضبط الحد الأدنى لإصدار حزمة تطوير البرامج (SDK)

  • اضبط قيود إصدار حزمة تطوير البرامج (SDK) لمشروعك على أن يعتمد على الإصدار 3 من Dart أو إصدار أحدث.

pubspec.yaml

environment:
  sdk: ^3.0.0

4. إعداد المشروع

في هذه الخطوة، يمكنك إنشاء ملفَي Dart أو تعديلهما:

  • ملف main.dart الذي يحتوي على أدوات للتطبيق
  • ملف data.dart الذي يوفّر بيانات التطبيق

ستواصل تعديل هذين الملفين في الخطوات اللاحقة.

تحديد بيانات التطبيق

  • أنشئ ملفًا جديدًا، lib/data.dart، وأضِف إليه الرمز التالي:

lib/data.dart

import 'dart:convert';

class Document {
  final Map<String, Object?> _json;
  Document() : _json = jsonDecode(documentJson);
}

const documentJson = '''
{
  "metadata": {
    "title": "My Document",
    "modified": "2023-05-10"
  },
  "blocks": [
    {
      "type": "h1",
      "text": "Chapter 1"
    },
    {
      "type": "p",
      "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
    },
    {
      "type": "checkbox",
      "checked": false,
      "text": "Learn Dart 3"
    }
  ]
}
''';

لنفترض أنّ هناك برنامجًا يتلقّى بيانات من مصدر خارجي، مثل دفق إدخال/إخراج أو طلب HTTP. في هذا الدرس التطبيقي حول الترميز، يمكنك تبسيط حالة الاستخدام الأكثر واقعية هذه من خلال محاكاة بيانات JSON الواردة باستخدام سلسلة متعددة الأسطر في المتغير documentJson.

يتم تحديد بيانات JSON في الفئة Document. في وقت لاحق من هذا الدرس التطبيقي حول الترميز، ستضيف الوظائف التي تعرض البيانات من ملف JSON الذي تم تحليله. تحدّد هذه الفئة الحقل _json وتهيّئه في الدالة الإنشائية.

تشغيل التطبيق

ينشئ الأمر flutter create الملف lib/main.dart كجزء من بنية ملفات Flutter التلقائية.

  1. لإنشاء نقطة بداية للتطبيق، استبدِل محتوى main.dart بالرمز التالي:

lib/main.dart

import 'package:flutter/material.dart';

import 'data.dart';

void main() {
  runApp(const DocumentApp());
}

class DocumentApp extends StatelessWidget {
  const DocumentApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(),
      home: DocumentScreen(document: Document()),
    );
  }
}

class DocumentScreen extends StatelessWidget {
  final Document document;

  const DocumentScreen({required this.document, super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Title goes here')),
      body: const Column(children: [Center(child: Text('Body goes here'))]),
    );
  }
}

أضفت التطبيقَين المصغّرَين التاليَين إلى التطبيق:

  • تُعدّ DocumentApp أحدث إصدار من التصميم المتعدد الأبعاد لتصميم واجهة المستخدم.
  • توفّر DocumentScreen التنسيق المرئي للصفحة باستخدام الأداة Scaffold.
  1. للتأكّد من أنّ كل شيء يعمل بسلاسة، شغِّل التطبيق على جهازك المضيف من خلال النقر على تشغيل وتصحيح الأخطاء:

زر &quot;التشغيل وتصحيح الأخطاء&quot;

  1. يختار Flutter تلقائيًا أي منصة مستهدَفة متاحة. لتغيير المنصة المستهدفة، اختَر النظام الأساسي الحالي في شريط الحالة:

أداة اختيار المنصة المستهدَفة في VS Code

من المفترض أن يظهر لك إطار فارغ يتضمّن العنصرَين title وbody المحدّدين في أداة DocumentScreen:

التطبيق الذي تم إنشاؤه في هذه الخطوة

5- إنشاء السجلات وإرجاعها

في هذه الخطوة، يمكنك استخدام السجلات لعرض قيم متعددة من طلب دالة. بعد ذلك، يمكنك استدعاء هذه الدالة في أداة DocumentScreen للوصول إلى القيم وعرضها في واجهة المستخدم.

إنشاء سجلّ وإرجاعه

  • في data.dart، أضِف طريقة جديدة لإرجاع القيمة إلى فئة Document باسم metadata تعرض سجلاً:

lib/data.dart

import 'dart:convert';

class Document {
  final Map<String, Object?> _json;
  Document() : _json = jsonDecode(documentJson);

  (String, {DateTime modified}) get metadata {           // Add from here...
    const title = 'My Document';
    final now = DateTime.now();

    return (title, modified: now);
  }                                                      // to here.
}

نوع القيمة التي تم إرجاعها لهذه الدالة هو سجلّ يتضمّن حقلَين، أحدهما من النوع String والآخر من النوع DateTime.

تنشئ عبارة return سجلاً جديدًا عن طريق تضمين القيمتين بين قوسين، (title, modified: now).

الحقل الأول هو حقل موضعي وبدون اسم، والحقل الثاني اسمه modified.

حقول سجلّ الوصول

  1. في أداة DocumentScreen، استدعِ طريقة إرجاع القيمة metadata في طريقة build حتى تتمكّن من الحصول على السجلّ والوصول إلى قيمه:

lib/main.dart

class DocumentScreen extends StatelessWidget {
  final Document document;

  const DocumentScreen({required this.document, super.key});

  @override
  Widget build(BuildContext context) {
    final metadataRecord = document.metadata;              // Add this line.

    return Scaffold(
      appBar: AppBar(title: Text(metadataRecord.$1)),      // Modify this line,
      body: Column(
        children: [                                        // And the following line.
          Center(child: Text('Last modified ${metadataRecord.modified}')),
        ],
      ),
    );
  }
}

تعرض طريقة الحصول على القيمة metadata سجلاً يتم تعيينه للمتغير المحلي metadataRecord. السجلات هي طريقة بسيطة وسهلة لإرجاع قيم متعددة من استدعاء دالة واحد وتعيينها إلى متغير.

للوصول إلى الحقول الفردية المضمّنة في هذا السجلّ، يمكنك استخدام بنية getter المضمّنة في السجلّات.

  • للحصول على حقل موضعي (حقل بدون اسم، مثل title)، استخدِم الدالة getter في السجلّ. يعرض هذا الإجراء الحقول التي ليس لها أسماء فقط.
  • لا تحتوي الحقول المسماة، مثل modified، على دالة جلب موضعية، لذا يمكنك استخدام اسمها مباشرةً، مثل metadataRecord.modified.

لتحديد اسم دالة إرجاع القيمة لحقل موضعي، ابدأ من $1 وتخطَّ الحقول المسماة. على سبيل المثال:

var record = (named: 'v', 'y', named2: 'x', 'z');
print(record.$1);                               // prints y
print(record.$2);                               // prints z
  1. أعِد التحميل السريع للاطّلاع على قيم JSON المعروضة في التطبيق، إذ يعيد مكوّن VS Code Dart الإضافي التحميل السريع في كل مرة تحفظ فيها ملفًا.

لقطة شاشة للتطبيق تعرض العنوان وتاريخ التعديل

يمكنك أن ترى أنّ كل حقل احتفظ بنوعه.

  • تأخذ الطريقة Text() سلسلة كمعامل أول.
  • الحقل modified هو DateTime، ويتم تحويله إلى String باستخدام تضمين السلسلة.

الطريقة الأخرى الآمنة من حيث الأنواع لعرض أنواع مختلفة من البيانات هي تحديد فئة، وهي أكثر تفصيلاً.

6. المطابقة والتفكيك باستخدام الأنماط

يمكن أن تجمع السجلات أنواعًا مختلفة من البيانات بكفاءة وتمرّرها بسهولة. يمكنك الآن تحسين الرمز باستخدام الأنماط.

يمثّل النمط بنية يمكن أن تتخذها قيمة واحدة أو أكثر، مثل مخطط. تتم مقارنة الأنماط بالقيم الفعلية لتحديد ما إذا كانت متطابقة.

عندما تتطابق بعض الأنماط، فإنّها تفكّك القيمة المطابقة من خلال استخراج البيانات منها. تتيح لك عملية تفكيك البنية استخراج القيم من عنصر لتحديدها كمتغيرات محلية، أو إجراء مطابقة إضافية عليها.

تقسيم سجلّ إلى متغيرات محلية

  1. أعِد تصميم طريقة build في DocumentScreen لاستدعاء metadata واستخدِمها لتهيئة تعريف متغير نمط:

lib/main.dart

class DocumentScreen extends StatelessWidget {
  final Document document;

  const DocumentScreen({required this.document, super.key});

  @override
  Widget build(BuildContext context) {
    final (title, modified: modified) = document.metadata;   // Modify

    return Scaffold(
      appBar: AppBar(title: Text(title)),                    // Modify from here...
      body: Column(children: [Center(child: Text('Last modified $modified'))]),
    );                                                       // To here.
  }
}

يحتوي نمط السجلّ (title, modified: modified) على نمطَين متغيّرَين يتطابقان مع حقول السجلّ الذي تعرضه الدالة metadata.

  • يتطابق التعبير مع النمط الفرعي لأنّ النتيجة هي سجلّ يحتوي على حقلَين، أحدهما يحمل الاسم modified.
  • بما أنّها تتطابق، فإنّ نمط تعريف المتغيّر يفكّك البنية التعبيرية، ويصل إلى قيمها ويربطها بمتغيّرات محلية جديدة من الأنواع والأسماء نفسها، String title وDateTime modified.

هناك اختصار عندما يكون اسم الحقل والمتغيّر الذي يملأه متطابقَين. أعِد تصميم طريقة build في DocumentScreen على النحو التالي.

lib/main.dart

class DocumentScreen extends StatelessWidget {
  final Document document;

  const DocumentScreen({required this.document, super.key});

  @override
  Widget build(BuildContext context) {
    final (title, :modified) = document.metadata;            // Modify

    return Scaffold(
      appBar: AppBar(title: Text(title)),
      body: Column(children: [Center(child: Text('Last modified $modified'))]),
    );
  }
}

إنّ بنية نمط المتغيّر :modified هي اختصار لـ modified: modified. إذا كنت تريد متغيرًا محليًا جديدًا باسم مختلف، يمكنك كتابة modified: localModified بدلاً من ذلك.

  1. أعِد التحميل السريع للاطّلاع على النتيجة نفسها كما في الخطوة السابقة. سيكون السلوك هو نفسه تمامًا، ولكنّك جعلت الرمز البرمجي أكثر إيجازًا.

7. استخدام الأنماط لاستخراج البيانات

في سياقات معيّنة، لا تتطابق الأنماط وتفكّك البنية فحسب، بل يمكنها أيضًا اتخاذ قرار بشأن ما يفعله الرمز، استنادًا إلى ما إذا كان النمط مطابقًا أم لا. تُسمّى هذه الأنماط القابلة للدحض.

إنّ نمط تعريف المتغيّر الذي استخدمته في الخطوة الأخيرة هو نمط غير قابل للدحض: يجب أن تتطابق القيمة مع النمط، وإلا سيحدث خطأ ولن يتم تفكيك البنية. فكر في أي عملية تعريف أو تعيين لمتغير، إذ لا يمكنك تعيين قيمة لمتغير إذا لم يكن من النوع نفسه.

من ناحية أخرى، تُستخدَم الأنماط القابلة للدحض في سياقات تدفّق التحكّم:

  • ويتوقّعون ألا تتطابق بعض القيم التي يقارنون بها.
  • وهي تهدف إلى التأثير في مسار التحكّم، استنادًا إلى ما إذا كانت القيمة متطابقة أم لا.
  • لا تتسبّب في إيقاف التنفيذ بسبب حدوث خطأ في حال عدم التطابق، بل تنتقل إلى العبارة التالية.
  • يمكنهم تفكيك المتغيرات وربطها التي يمكن استخدامها فقط عندما تتطابق

قراءة قيم JSON بدون أنماط

في هذا القسم، يمكنك قراءة البيانات بدون مطابقة الأنماط لمعرفة كيف يمكن أن تساعدك الأنماط في التعامل مع بيانات JSON.

  • استبدِل الإصدار السابق من metadata بإصدار يقرأ القيم من خريطة _json. انسخ هذه النسخة من metadata والصقها في الصف Document:

lib/data.dart

class Document {
  final Map<String, Object?> _json;
  Document() : _json = jsonDecode(documentJson);

  (String, {DateTime modified}) get metadata {
    if (_json.containsKey('metadata')) {                     // Modify from here...
      final metadataJson = _json['metadata'];
      if (metadataJson is Map) {
        final title = metadataJson['title'] as String;
        final localModified = DateTime.parse(
          metadataJson['modified'] as String,
        );
        return (title, modified: localModified);
      }
    }
    throw const FormatException('Unexpected JSON');          // to here.
  }
}

يتحقّق هذا الرمز من أنّ البيانات منظَّمة بشكل صحيح بدون استخدام أنماط. في خطوة لاحقة، ستستخدم ميزة "مطابقة الأنماط" لإجراء عملية التحقّق نفسها باستخدام رمز أقل. يُجري هذا الإجراء ثلاث عمليات تحقّق قبل تنفيذ أي إجراء آخر:

  • يحتوي ملف JSON على بنية البيانات التي تتوقّعها: if (_json.containsKey('metadata'))
  • تحتوي البيانات على النوع الذي تتوقّعه: if (metadataJson is Map)
  • أنّ البيانات ليست فارغة، وهو ما يتم تأكيده ضمنيًا في عملية التحقّق السابقة

قراءة قيم JSON باستخدام نمط خريطة

باستخدام نمط قابل للدحض، يمكنك التأكّد من أنّ ملف JSON يتضمّن البنية المتوقّعة باستخدام نمط الخريطة.

  • استبدِل الإصدار السابق من metadata بهذا الرمز:

lib/data.dart

class Document {
  final Map<String, Object?> _json;
  Document() : _json = jsonDecode(documentJson);

  (String, {DateTime modified}) get metadata {
    if (_json case {                                         // Modify from here...
      'metadata': {'title': String title, 'modified': String localModified},
    }) {
      return (title, modified: DateTime.parse(localModified));
    } else {
      throw const FormatException('Unexpected JSON');
    }                                                        // to here.
  }
}

هنا، يمكنك الاطّلاع على نوع جديد من عبارات if (تم تقديمه في Dart 3)، وهو if-case. لا يتم تنفيذ نص الحالة إلا إذا كان نمط الحالة يتطابق مع البيانات في _json. تُجري هذه المطابقة عمليات التحقّق نفسها التي كتبتها في الإصدار الأول من metadata للتحقّق من صحة ملف JSON الوارد. يتحقّق هذا الرمز من صحة ما يلي:

  • _json هو نوع من الخرائط.
  • يحتوي _json على مفتاح metadata.
  • _json ليس فارغًا.
  • _json['metadata'] هو أيضًا نوع من الخرائط.
  • يحتوي _json['metadata'] على المفتاحين title وmodified.
  • title وlocalModified هما سلسلتان وليستا فارغتين.

إذا لم تتطابق القيمة، يرفض النمط (يرفض مواصلة التنفيذ) وينتقل إلى عبارة else. في حال نجاح المطابقة، يفكّك النمط قيم title وmodified من الخريطة ويربطها بمتغيرات محلية جديدة.

للاطّلاع على قائمة كاملة بالأنماط، يُرجى الرجوع إلى الجدول في قسم "الأنماط" ضمن مواصفات الميزة.

8. إعداد التطبيق لمزيد من الأنماط

حتى الآن، يمكنك معالجة الجزء metadata من بيانات JSON. في هذه الخطوة، يمكنك تحسين منطق نشاطك التجاري قليلاً للتعامل مع البيانات في القائمة blocks وعرضها في تطبيقك.

{
  "metadata": {
    // ...
  },
  "blocks": [
    {
      "type": "h1",
      "text": "Chapter 1"
    },
    // ...
  ]
}

إنشاء صف يخزّن البيانات

  • أضِف فئة جديدة، Block، إلى data.dart، والتي تُستخدَم لقراءة البيانات وتخزينها لأحد الأقسام في بيانات JSON.

lib/data.dart

class Block {
  final String type;
  final String text;
  Block(this.type, this.text);

  factory Block.fromJson(Map<String, dynamic> json) {
    if (json case {'type': final type, 'text': final text}) {
      return Block(type, text);
    } else {
      throw const FormatException('Unexpected JSON format');
    }
  }
}

يستخدم منشئ المصنع fromJson() عبارة if-case نفسها مع نمط الخريطة الذي رأيته من قبل.

ستلاحظ أنّ بيانات JSON تبدو مثل النمط المتوقّع، على الرغم من أنّها تتضمّن جزءًا إضافيًا من المعلومات يُسمى checked غير مضمّن في النمط. يرجع ذلك إلى أنّه عند استخدام هذه الأنواع من الأنماط (المعروفة باسم "أنماط الخرائط")، فإنّها تهتم فقط بالعناصر المحدّدة التي حدّدتها في النمط وتتجاهل أي شيء آخر في البيانات.

إرجاع قائمة بعناصر Block

  • بعد ذلك، أضِف دالة جديدة، getBlocks()، إلى الفئة Document. تعمل getBlocks() على تحليل JSON إلى مثيلات من فئة Block وتعرض قائمة بالكتل لعرضها في واجهة المستخدم:

lib/data.dart

class Document {
  final Map<String, Object?> _json;
  Document() : _json = jsonDecode(documentJson);

  (String, {DateTime modified}) get metadata {
    if (_json case {
      'metadata': {'title': String title, 'modified': String localModified},
    }) {
      return (title, modified: DateTime.parse(localModified));
    } else {
      throw const FormatException('Unexpected JSON');
    }
  }

  List<Block> getBlocks() {                                  // Add from here...
    if (_json case {'blocks': List blocksJson}) {
      return [for (final blockJson in blocksJson) Block.fromJson(blockJson)];
    } else {
      throw const FormatException('Unexpected JSON format');
    }
  }                                                          // to here.
}

تعرض الدالة getBlocks() قائمة بعناصر Block، والتي تستخدمها لاحقًا لإنشاء واجهة المستخدم. تُجري عبارة if-case مألوفة عملية التحقّق من الصحة وتحوّل قيمة البيانات الوصفية blocks إلى List جديدة باسم blocksJson (بدون أنماط، ستحتاج إلى طريقة toList() للتحويل).

يحتوي حرفية القائمة على collection for من أجل ملء القائمة الجديدة بعناصر Block.

لا يقدّم هذا القسم أي ميزات متعلّقة بالأنماط لم يسبق لك تجربتها في هذا الدرس التطبيقي حول الترميز. في الخطوة التالية، عليك الاستعداد لعرض عناصر القائمة في واجهة المستخدم.

9- استخدام الأنماط لعرض المستند

لقد تمكّنت الآن من تقسيم بيانات JSON وإعادة تجميعها بنجاح، وذلك باستخدام عبارة if-case وأنماط قابلة للدحض. لكنّ عبارة if-case هي مجرد واحدة من التحسينات على بنى التحكّم في التدفق التي تأتي مع الأنماط. الآن، طبِّق معرفتك بأنماط الرفض على عبارات التبديل.

التحكّم في المحتوى المعروض باستخدام الأنماط مع عبارات switch

  • في main.dart، أنشئ أداة جديدة، BlockWidget، تحدّد نمط كلّ كتلة استنادًا إلى الحقل type.

lib/main.dart

class BlockWidget extends StatelessWidget {
  final Block block;

  const BlockWidget({required this.block, super.key});

  @override
  Widget build(BuildContext context) {
    TextStyle? textStyle;
    switch (block.type) {
      case 'h1':
        textStyle = Theme.of(context).textTheme.displayMedium;
      case 'p' || 'checkbox':
        textStyle = Theme.of(context).textTheme.bodyMedium;
      case _:
        textStyle = Theme.of(context).textTheme.bodySmall;
    }

    return Container(
      margin: const EdgeInsets.all(8),
      child: Text(block.text, style: textStyle),
    );
  }
}

تعمل عبارة switch في الطريقة build على تفعيل الحقل type الخاص بالعنصر block.

  1. يستخدم بيان الحالة الأول نمط سلسلة ثابتة. تتم مطابقة النمط إذا كانت block.type تساوي القيمة الثابتة h1.
  2. يستخدم بيان الحالة الثاني نمطًا منطقيًا أو مع نمطين ثابتين للسلسلة كنماذج فرعية. يتطابق النمط إذا تطابق block.type مع أي من الأنماط الفرعية p أو checkbox.
  1. الحالة الأخيرة هي نمط حرف بدل، _. تطابق أحرف البدل في عبارات switch كل ما عدا ذلك. وتعمل هذه الحالات بالطريقة نفسها التي تعمل بها عبارات default، والتي لا يزال مسموحًا بها في عبارات switch (مع أنّها أكثر تفصيلاً قليلاً).

يمكن استخدام أنماط أحرف البدل في أي مكان يُسمح فيه بنمط، مثلاً في نمط تعريف متغير: var (title, _) = document.metadata;

في هذا السياق، لا يربط حرف البدل أي متغيّر. يتم تجاهل الحقل الثاني.

في القسم التالي، ستتعرّف على المزيد من ميزات المفتاح بعد عرض كائنات Block.

عرض محتوى المستند

أنشِئ متغيّرًا محليًا يحتوي على قائمة بعناصر Block من خلال استدعاء getBlocks() في طريقة build لعنصر واجهة المستخدم DocumentScreen.

  1. استبدِل طريقة build الحالية في DocumentationScreen بهذه النسخة:

lib/main.dart

class DocumentScreen extends StatelessWidget {
  final Document document;

  const DocumentScreen({required this.document, super.key});

  @override
  Widget build(BuildContext context) {
    final (title, :modified) = document.metadata;
    final blocks = document.getBlocks();                           // Add this line

    return Scaffold(
      appBar: AppBar(title: Text(title)),
      body: Column(
        children: [
          Text('Last modified: $modified'),                        // Modify from here
          Expanded(
            child: ListView.builder(
              itemCount: blocks.length,
              itemBuilder: (context, index) {
                return BlockWidget(block: blocks[index]);
              },
            ),
          ),                                                       // to here.
        ],
      ),
    );
  }
}

ينشئ السطر BlockWidget(block: blocks[index]) أداة BlockWidget لكل عنصر في قائمة الحظر التي تم إرجاعها من الطريقة getBlocks().

  1. شغِّل التطبيق، ومن المفترض أن تظهر لك الوحدات على الشاشة:

التطبيق الذي يعرض المحتوى من قسم &quot;الفقرات&quot; في بيانات JSON

10. استخدام تعبيرات switch

تضيف الأنماط الكثير من الإمكانات إلى switch وcase. لإتاحة استخدامها في المزيد من الأماكن، تتضمّن Dart تعبيرات switch. يمكن أن توفّر سلسلة من الحالات قيمة مباشرة لتعيين متغيّر أو عبارة return.

تحويل عبارة switch إلى تعبير switch

يوفّر محلّل Dart مساعدات لمساعدتك في إجراء تغييرات على الرمز البرمجي.

  1. حرِّك المؤشر إلى عبارة التبديل من القسم السابق.
  2. انقر على رمز المصباح لعرض المساعدات المتاحة.
  3. اختَر أداة المساعدة التحويل إلى تعبير switch.

تتوفّر ميزة المساعدة &quot;التحويل إلى تعبير switch&quot; في VS Code.

يبدو الإصدار الجديد من هذا الرمز على النحو التالي:

lib/main.dart

class BlockWidget extends StatelessWidget {
  final Block block;

  const BlockWidget({required this.block, super.key});

  @override
  Widget build(BuildContext context) {
    TextStyle? textStyle;                                          // Modify from here
    textStyle = switch (block.type) {
      'h1' => Theme.of(context).textTheme.displayMedium,
      'p' || 'checkbox' => Theme.of(context).textTheme.bodyMedium,
      _ => Theme.of(context).textTheme.bodySmall,
    };                                                             // to here.

    return Container(
      margin: const EdgeInsets.all(8),
      child: Text(block.text, style: textStyle),
    );
  }
}

يبدو تعبير التبديل مشابهًا لعبارة التبديل، ولكنّه يزيل الكلمة الأساسية case ويستخدم => لفصل النمط عن نص الحالة. على عكس عبارات switch، تعرض تعبيرات switch قيمة ويمكن استخدامها في أي مكان يمكن استخدام تعبير فيه.

11. استخدام أنماط الكائنات

‫Dart هي لغة تعتمد على العناصر، لذا تنطبق الأنماط على جميع العناصر. في هذه الخطوة، عليك تفعيل نمط عنصر وتفكيك خصائص العنصر لتحسين منطق عرض التاريخ في واجهة المستخدم.

استخراج الخصائص من أنماط العناصر

في هذا القسم، ستعمل على تحسين طريقة عرض تاريخ آخر تعديل باستخدام الأنماط.

  • أضِف طريقة formatDate إلى main.dart:

lib/main.dart

String formatDate(DateTime dateTime) {
  final today = DateTime.now();
  final difference = dateTime.difference(today);

  return switch (difference) {
    Duration(inDays: 0) => 'today',
    Duration(inDays: 1) => 'tomorrow',
    Duration(inDays: -1) => 'yesterday',
    Duration(inDays: final days, isNegative: true) => '${days.abs()} days ago',
    Duration(inDays: final days) => '$days days from now',
  };
}

يعرض هذا الإجراء تعبير تبديل يتم التبديل فيه استنادًا إلى القيمة difference، وهي عنصر Duration. تمثّل هذه السمة الفترة الزمنية بين today والقيمة modified من بيانات JSON.

تستخدم كل حالة من حالات تعبير التبديل نمط كائن يطابق من خلال استدعاء دوال الجلب في خصائص الكائن inDays وisNegative. يبدو أنّ بنية الجملة قد تنشئ عنصر Duration، ولكنّها في الواقع تصل إلى الحقول في العنصر difference.

تستخدم الحالات الثلاث الأولى الأنماط الفرعية الثابتة 0 و1 و-1 لمطابقة سمة العنصر inDays وعرض السلسلة المطابقة.

تتعامل الحالتان الأخيرتان مع المدد الزمنية التي تتجاوز اليوم والأمس والغد:

  • إذا كانت السمة isNegative تتطابق مع نمط الثابت المنطقي true، ما يعني أنّ تاريخ التعديل كان في الماضي، سيتم عرض منذ أيام.
  • إذا لم يرصد هذا الشرط الفرق، يجب أن تكون المدة عددًا موجبًا من الأيام (لا حاجة إلى التحقّق من ذلك بشكل صريح باستخدام isNegative: false)، وبالتالي يكون تاريخ التعديل في المستقبل ويتم عرض عدد الأيام من الآن.

إضافة منطق التنسيق للأسابيع

  • أضِف حالتين جديدتين إلى دالة التنسيق لتحديد المدد الأطول من 7 أيام حتى تتمكّن واجهة المستخدم من عرضها على أنّها أسابيع:

lib/main.dart

String formatDate(DateTime dateTime) {
  final today = DateTime.now();
  final difference = dateTime.difference(today);

  return switch (difference) {
    Duration(inDays: 0) => 'today',
    Duration(inDays: 1) => 'tomorrow',
    Duration(inDays: -1) => 'yesterday',
    Duration(inDays: final days) when days > 7 => '${days ~/ 7} weeks from now', // Add from here
    Duration(inDays: final days) when days < -7 =>
      '${days.abs() ~/ 7} weeks ago',                                            // to here.
    Duration(inDays: final days, isNegative: true) => '${days.abs()} days ago',
    Duration(inDays: final days) => '$days days from now',
  };
}

يقدّم هذا الرمز عبارات الحراسة:

  • تستخدم عبارة الحراسة الكلمة الأساسية when بعد نمط الحالة.
  • يمكن استخدامها في عبارات if وعبارات switch وتعبيرات switch.
  • لا يضيفون شرطًا إلى نمط إلا بعد مطابقته.
  • إذا تم تقييم شرط الحراسة على أنّه خطأ، سيتم دحض النمط بأكمله، وسيتم الانتقال إلى الحالة التالية.

إضافة التاريخ المنسَّق حديثًا إلى واجهة المستخدم

  1. أخيرًا، عدِّل طريقة build في DocumentScreen لاستخدام الدالة formatDate:

lib/main.dart

class DocumentScreen extends StatelessWidget {
  final Document document;

  const DocumentScreen({required this.document, super.key});

  @override
  Widget build(BuildContext context) {
    final (title, :modified) = document.metadata;
    final formattedModifiedDate = formatDate(modified);            // Add this line
    final blocks = document.getBlocks();

    return Scaffold(
      appBar: AppBar(title: Text(title)),
      body: Column(
        children: [
          Text('Last modified: $formattedModifiedDate'),           // Modify this line
          Expanded(
            child: ListView.builder(
              itemCount: blocks.length,
              itemBuilder: (context, index) {
                return BlockWidget(block: blocks[index]);
              },
            ),
          ),
        ],
      ),
    );
  }
}
  1. إعادة التحميل السريع للاطّلاع على التغييرات في تطبيقك:

التطبيق الذي يعرض السلسلة &quot;آخر تعديل: قبل أسبوعَين&quot; باستخدام الدالة formatDate().

12. إغلاق صف للتبديل الشامل

لاحظ أنّك لم تستخدم حرف بدل أو حالة تلقائية في نهاية عبارة التبديل الأخيرة. على الرغم من أنّه من الممارسات الجيدة تضمين حالة للقيم التي قد لا يتم التعرّف عليها، لا بأس في مثال بسيط مثل هذا لأنّك تعرف الحالات التي حدّدتها والتي تمثّل جميع القيم المحتملة inDays التي يمكن أن تتخذها.

عندما يتم التعامل مع كل حالة في عبارة switch، يُطلق عليها اسم عبارة switch شاملة. على سبيل المثال، يكون التبديل إلى النوع bool شاملاً عندما يتضمّن حالات true وfalse. يكون التبديل إلى نوع enum شاملاً أيضًا عندما تكون هناك حالات لكل قيمة من قيم التعداد، لأنّ التعدادات تمثّل عددًا ثابتًا من القيم الثابتة.

وسّعت الإصدار 3 من Dart نطاق التحقّق من الشمولية ليشمل الكائنات والتسلسلات الهرمية للفئات باستخدام معدِّل الفئة الجديد sealed. أعِد تصميم فئة Block كفئة أساسية محكمة الإغلاق.

إنشاء الفئات الفرعية

  • في data.dart، أنشئ ثلاث فئات جديدة، وهي HeaderBlock وParagraphBlock وCheckboxBlock، توسّع Block:

lib/data.dart

class HeaderBlock extends Block {
  final String text;
  HeaderBlock(this.text);
}

class ParagraphBlock extends Block {
  final String text;
  ParagraphBlock(this.text);
}

class CheckboxBlock extends Block {
  final String text;
  final bool isChecked;
  CheckboxBlock(this.text, this.isChecked);
}

يتوافق كل من هذه الصفوف مع قيم type المختلفة من ملف JSON الأصلي: 'h1' و'p' و'checkbox'.

إغلاق الفئة الرئيسية

  • ضع علامة sealed على الفئة Block. بعد ذلك، أعِد تصميم عبارة if-case كتعبير switch يعرض الفئة الفرعية المتوافقة مع type المحدّدة في JSON:

lib/data.dart

sealed class Block {
  Block();

  factory Block.fromJson(Map<String, Object?> json) {
    return switch (json) {
      {'type': 'h1', 'text': String text} => HeaderBlock(text),
      {'type': 'p', 'text': String text} => ParagraphBlock(text),
      {'type': 'checkbox', 'text': String text, 'checked': bool checked} =>
        CheckboxBlock(text, checked),
      _ => throw const FormatException('Unexpected JSON format'),
    };
  }
}

الكلمة الرئيسية sealed هي معدِّل فئة يعني أنّه يمكنك توسيع هذه الفئة أو تنفيذها في المكتبة نفسها فقط. بما أنّ المحلّل يعرف الأنواع الفرعية لهذه الفئة، فإنّه يُبلغ عن خطأ إذا لم يغطِّ مفتاح تبديل أحدها ولم يكن شاملاً.

استخدام تعبير switch لعرض عناصر واجهة المستخدم

  1. عدِّل الفئة BlockWidget في main.dart باستخدام تعبير switch يستخدِم أنماط الكائنات لكل حالة:

lib/main.dart

class BlockWidget extends StatelessWidget {
  final Block block;

  const BlockWidget({required this.block, super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.all(8),
      child: switch (block) {
        HeaderBlock(:final text) => Text(
          text,
          style: Theme.of(context).textTheme.displayMedium,
        ),
        ParagraphBlock(:final text) => Text(text),
        CheckboxBlock(:final text, :final isChecked) => Row(
          children: [
            Checkbox(value: isChecked, onChanged: (_) {}),
            Text(text),
          ],
        ),
      },
    );
  }
}

في الإصدار الأول من BlockWidget، فعّلت حقل كائن Block لعرض TextStyle. يمكنك الآن تبديل مثيل من العنصر Block نفسه والمطابقة مع أنماط العناصر التي تمثّل فئاته الفرعية، واستخراج خصائص العنصر في هذه العملية.

يمكن لمحلّل Dart التحقّق من أنّه يتم التعامل مع كل فئة فرعية في تعبير switch لأنّك جعلت Block فئة محكمة الإغلاق.

يُرجى العِلم أيضًا أنّ استخدام تعبير switch هنا يتيح لك تمرير النتيجة مباشرةً إلى العنصر child، على عكس عبارة return المنفصلة التي كانت مطلوبة من قبل.

  1. أعِد التحميل السريع للاطّلاع على بيانات JSON الخاصة بمربّع الاختيار التي يتم عرضها للمرة الأولى:

التطبيق الذي يعرض مربّع الاختيار &quot;تعرَّف على Dart 3&quot;

13. تهانينا

لقد جرّبت بنجاح الأنماط والسجلات والعبارات المحسّنة switch وcase والفئات المحكمة. لقد تناولت الكثير من المعلومات، ولكنك لم تتطرّق إلا إلى القليل من هذه الميزات. لمزيد من المعلومات حول الأنماط، يُرجى الاطّلاع على مواصفات الميزة.

إنّ أنواع الأنماط المختلفة والسياقات المختلفة التي يمكن أن تظهر فيها، بالإضافة إلى إمكانية تضمين أنماط فرعية، تجعل الاحتمالات في السلوك تبدو لا نهائية. لكنّها سهلة الرؤية.

يمكنك تخيّل جميع أنواع طرق عرض المحتوى في Flutter باستخدام الأنماط. باستخدام الأنماط، يمكنك استخراج البيانات بأمان لإنشاء واجهة المستخدم في بضعة أسطر من الرموز البرمجية.

ما هي الخطوات التالية؟

  • يمكنك الاطّلاع على المستندات حول الأنماط والسجلات والمفاتيح والحالات المحسّنة ومعدّلات الفئات في قسم اللغة من مستندات Dart.

المستندات المرجعية

يمكنك الاطّلاع على رمز نموذجي كامل، خطوة بخطوة، في مستودع flutter/codelabs.

للاطّلاع على مواصفات تفصيلية لكل ميزة جديدة، يمكنك الرجوع إلى مستندات التصميم الأصلية: