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

الاطّلاع على أنماط Dart وسجلّاتها

لمحة عن هذا الدرس التطبيقي حول الترميز

subjectتاريخ التعديل الأخير: أبريل 4, 2025
account_circleتأليف: John Ryan and Marya Belanger

1. مقدمة

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

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

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

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

ما ستُنشئه

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

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

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

ما ستتعرّف عليه

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

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 تعرض المشروع الذي تم إنشاؤه باستخدام الأمر flutter create

ضبط الحد الأدنى لإصدار حزمة SDK

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

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

صورة لزر &quot;التشغيل وتصحيح الأخطاء&quot;، المتوفّر في قسم &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: [
          Center(
            child: Text(
              'Last modified ${metadataRecord.modified}',  // And this one.
            ),
          ),
        ],
      ),
    );
  }
}

تُرجع طريقة الحصول على metadata سجلاً، ويتم تعيينه للمتغيّر المحلي metadataRecord. السجلّات هي طريقة خفيفة وسهلة لعرض قيم متعدّدة من طلب دالة واحدة وتحديدها لمتغيّر.

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

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

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

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

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

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

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

عرض قائمة بكائنات 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() للتحويل).

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

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

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

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

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

  • في 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,
      ),
    );
  }
}

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

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 وstatements switch وexpressions 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.

عندما يتمّ التعامل مع كلّ حالة في مفتاح، يُسمّى ذلك مفتاحًا شاملاً. على سبيل المثال، يكون تفعيل نوع 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'.

إغلاق الفئة الفائقة

  • ضَع علامة sealed على فئة Block. بعد ذلك، أعِد تنظيم حالة if-case كتعبير تبديل يعرض الفئة الفرعية المقابلة للقيمة 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 فئة مختومة.

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

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

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

13. تهانينا

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

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

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

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

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

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

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

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