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

1. مقدمة

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

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

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

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

ما الذي ستقوم ببنائه

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

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

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

المعلومات التي ستطّلع عليها

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

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

  1. ثبِّت Flutter SDK.
  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 patterns_codelab

لقطة شاشة لـ VS Code تعرض المشروع الذي تم إنشاؤه باستخدام Flutter create الأمر.

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

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

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(useMaterial3: true),
      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 على إعداد أحدث إصدار من Material Design لتخصيص واجهة المستخدم.
  • يوفر DocumentScreen التنسيق المرئي للصفحة باستخدام تطبيق Scaffold المصغّر.
  1. للتأكّد من سير الأمور بسلاسة، شغِّل التطبيق على الجهاز المضيف بالنقر على التشغيل وتصحيح الأخطاء:

صورة &quot;التشغيل وتصحيح الأخطاء&quot; متاح في قسم &quot;التشغيل وتصحيح الأخطاء&quot; من شريط النشاط على الجانب الأيمن.

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

لقطة شاشة لأداة اختيار المنصّة المستهدَفة في رمز VS.

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

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

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

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

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

  • في data.dart، أضِف طريقة دالة getter جديدة إلى فئة المستند المسماة 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.

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

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

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

  1. في التطبيق المصغّر DocumentScreen، يمكنك استدعاء طريقة getter (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: [
          Center(
            child: Text(
              'Last modified ${metadataRecord.modified}',  // And this one.
            ),
          ),
        ],
      ),
    );
  }
}

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

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

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

لتحديد اسم دالة getter لحقل موضعي، ابدأ من $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 حقل التاريخ والوقت، ويتم تحويله إلى 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
      ),
      body: Column(
        children: [
          Center(
            child: Text(
              'Last modified $modified',                     // Modify
            ),
          ),
        ],
      ),
    );
  }
}

يحتوي نمط السجل (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                                                // Modify from here...
        case {
          '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() حالة الحالة نفسها مع نمط خريطة سبق أن رأيته.

لاحظ أن json يطابق نمط الخريطة، على الرغم من أنه لا يتم احتساب أحد المفاتيح، checked، في النمط. تتجاهل أنماط الخريطة أي إدخالات في كائن الخريطة لم يتم احتسابها بشكل صريح في النمط.

عرض قائمة بكائنات الحظر

  • أضِف بعد ذلك دالة جديدة 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() لبثّ المحتوى).

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

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

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

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

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

  • في 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. الحالة الأخيرة هي نمط حرف بدل، _. تتطابق أحرف البدل في حالات المبدل مع أي شيء آخر. فهي تعمل بالطريقة نفسها التي تتّبعها فقرات default، ولكن لا يزال مسموحًا باستخدامها في عبارات التبديل (لأنّها مُطوّلة بعض الشيء).

يمكن استخدام أنماط أحرف البدل حيثما يُسمح بالنمط، على سبيل المثال، بنمط تعريف متغيّر: 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 وcase. لجعلها قابلة للاستخدام في المزيد من الأماكن، تحتوي Dart على تعابير تبديل. يمكن أن توفر سلسلة من الحالات قيمة مباشرة لتعيين متغير أو عبارة إرجاع.

تحويل جملة أمر التبديل إلى تعبير تبديل

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

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

لقطة شاشة لتعبير &quot;التحويل إلى مفتاح التبديل&quot; المساعدة المتوفرة في رمز VS.

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

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 ويستخدم => لفصل النمط عن نص الحالة. على عكس عبارات مفتاح التبديل، تُرجع تعبيرات التبديل قيمة ويمكن استخدامها في أي مكان يمكن فيه استخدام تعبير.

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.

تستخدم كل حالة من حالات تعبير مفتاح التحكّم نمط كائن يطابق من خلال استدعاء دالة getters على خصائص الكائن 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، وتبديل العبارات، وتبديل التعبيرات.
  • وهي تضيف شرطًا إلى نمط فقط بعد مطابقته.
  • وإذا تم تقييم عبارة guard على "خطأ"، يتم رفض النمط بأكمله، وينتقل التنفيذ إلى الحالة التالية.

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

  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.

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

وسّع Dart 3 التحقق من الشمولية ليشمل الكائنات والتسلسلات الهرمية للفئات باستخدام معدِّل الفئة الجديد 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'.

ختم الجوائز الرائعة

  • ضَع علامة على الصف Block على أنّه sealed. بعد ذلك، يجب إعادة ضبط الحالة if-to على أنّها تعبير تبديل يعرض الفئة الفرعية المقابلة للسمة 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 هي معدِّل الفئة، ما يعني أنّه يمكنك توسيع نطاق هذه الفئة أو تنفيذها في المكتبة نفسها فقط. ونظرًا لأن المحلل يعرف الأنواع الفرعية لهذه الفئة، فإنه يبلغ عن خطأ إذا فشل التبديل في تغطية أحدها ولا يكون شاملاً.

استخدام تعبير مفتاح تحكّم لعرض التطبيقات المصغّرة

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

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 التحقق من التعامل مع كل فئة فرعية في تعبير مفتاح التبديل لأنك جعلت Block فئة مختومة.

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

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

لقطة شاشة للتطبيق تعرض مربّع الاختيار &quot;Learn Dart 3&quot;

13. تهانينا

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

فأنواع الأنماط المختلفة، والسياقات المختلفة التي يمكن أن تظهر فيها، والدمج المحتمل للأنماط الفرعية، تجعل الاحتمالات في السلوك لا حصر لها. ولكن من السهل رؤيتها.

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

الخطوات التالية

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

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

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

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