لمحة عن هذا الدرس التطبيقي حول الترميز
1. مقدمة
تضيف Dart 3 نماذج إلى اللغة، وهي فئة جديدة ورئيسية من القواعد النحوية. بالإضافة إلى هذه الطريقة الجديدة لكتابة رموز Dart، هناك العديد من التحسينات الأخرى على اللغة، بما في ذلك
- السجلّات لتجميع البيانات من أنواع مختلفة
- عوامل تعديل الصف للتحكّم في الوصول
- تعبيرات التبديل وعبارات if-case الجديدة
وتوفّر هذه الميزات خيارات إضافية عند كتابة رمز Dart. في هذا الدرس التطبيقي حول الترميز، ستتعرّف على كيفية استخدامها لجعل رمزك أكثر كثافة وسلاسة ومرونة.
يفترض هذا الدليل التعليمي أنّك على دراية بإطار عمل Flutter ولغة Dart. إذا كنت بحاجة إلى مراجعة الأساسيات، يمكنك الاطّلاع على المراجع التالية:
ما ستُنشئه
ينشئ هذا الدليل التعليمي للترميز تطبيقًا يعرض مستند JSON في Flutter. يحاكي التطبيق تنسيق JSON القادم من مصدر خارجي. يحتوي ملف JSON على بيانات المستند، مثل تاريخ التعديل والعنوان والعناوين والفقرات. يمكنك كتابة رمز لتعبئة البيانات بدقة في السجلات حتى يمكن نقلها وفك تشفيرها في أي مكان تحتاج فيه إلى تطبيقات Flutter المصغّرة.
بعد ذلك، يمكنك استخدام الأنماط لإنشاء التطبيق المصغّر المناسب عندما تتطابق القيمة مع هذا النمط. ويمكنك أيضًا الاطّلاع على كيفية استخدام الأنماط لإزالة بنية البيانات إلى متغيّرات محلية.
ما ستتعرّف عليه
- كيفية إنشاء سجلّ يخزّن قيمًا متعددة بأنواع مختلفة
- كيفية عرض قيم متعدّدة من دالة باستخدام سجلّ
- كيفية استخدام الأنماط لمطابقة البيانات والتحقّق منها وإزالة بنيتها من السجلات والعناصر الأخرى
- كيفية ربط القيم التي تتطابق مع النمط بمتغيّرات جديدة أو حالية
- كيفية استخدام إمكانات التعبيرات وجمل "التبديل" وجمل "الحالة إذا" الجديدة
- كيفية الاستفادة من التحقّق من الشمولية لضمان معالجة كل حالة في عبارة أو تعبير تبديل
2. إعداد البيئة
- ثبِّت حزمة تطوير البرامج (SDK) من Flutter.
- إعداد محرِّر، مثل Visual Studio Code (VS Code)
- اتّبِع خطوات إعداد النظام الأساسي لنظام أساسي مستهدف واحد على الأقل (iOS أو Android أو الكمبيوتر المكتبي أو متصفّح الويب).
3. إنشاء المشروع
قبل التعمّق في الأنماط والسجلّات والميزات الجديدة الأخرى، ننصحك بإنشاء مشروع Flutter بسيط تكتب فيه كل الرموز البرمجية.
إنشاء مشروع Flutter
- استخدِم الأمر
flutter create
لإنشاء مشروع جديد باسمpatterns_codelab
. تمنع العلامة--empty
إنشاء تطبيق العداد العادي في ملفlib/main.dart
، والذي عليك إزالته على أي حال.
flutter create --empty patterns_codelab
- بعد ذلك، افتح الدليل
patterns_codelab
باستخدام VS Code.
code patterns_codelab
ضبط الحد الأدنى لإصدار حزمة 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 التلقائية.
- لإنشاء نقطة بداية للتطبيق، استبدِل محتوى
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
.
- للتأكّد من أنّ كل شيء يعمل بسلاسة، يمكنك تشغيل التطبيق على جهاز المضيف بالنقر على التشغيل وتصحيح الأخطاء:
- يختار Flutter تلقائيًا أي منصة مستهدَفة متاحة. لتغيير النظام الأساسي المستهدَف، اختَر النظام الأساسي الحالي في شريط الحالة:
من المفترض أن يظهر لك إطار فارغ يتضمّن عنصرَي 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
.
الوصول إلى حقول السجلّ
- في التطبيق المصغّر
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
- إعادة التحميل السريع للاطّلاع على قيم JSON المعروضة في التطبيق. يُجري المكوّن الإضافي Dart في VS Code إعادة تحميل سريعة في كل مرة تحفظ فيها ملفًا.
يمكنك ملاحظة أنّ كل حقل احتفظ بنوعه.
- تستخدِم الطريقة
Text()
سلسلة كوسيطة أولى. - الحقل
modified
هو تاريخ ووقت، ويتم تحويله إلىString
باستخدام إدراج سلسلة.
الطريقة الأخرى الآمنة من حيث النوع لعرض أنواع مختلفة من البيانات هي تحديد فئة، وهي أكثر تفصيلاً.
6. المطابقة وإزالة البنية باستخدام الأنماط
يمكن للسجلات جمع أنواع مختلفة من البيانات بكفاءة ونقلها بسهولة. يمكنك الآن تحسين الرمز باستخدام النماذج.
يمثّل النمط بنية يمكن أن تتّخذ قيمة واحدة أو أكثر، مثل مخطّط. تتم مقارنة الأنماط بالقيم الفعلية لتحديد ما إذا كانت مطابقة.
عند تطابق بعض الأنماط، تزيل البنية من القيمة المطابقة عن طريق سحب البيانات منها. يتيح لك تحليل البنية فك القيم من عنصر لتخصيصها للمتغيّرات المحلية أو إجراء المزيد من المطابقة عليها.
تحليل بنية سجلّ إلى متغيّرات محلية
- أعِد صياغة طريقة
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
بدلاً من ذلك.
- يمكنك إعادة التحميل السريع للاطّلاع على النتيجة نفسها التي ظهرت في الخطوة السابقة. لا يتغيّر السلوك على الإطلاق، بل إنّك جعلت الرمز أكثر إيجازًا.
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
.
- يستخدم بيان الحالة الأول نمط سلسلة ثابت. يتطابق النمط إذا كانت
block.type
مساوية للقيمة الثابتةh1
. - يستخدم بيان الحالة الثاني نمط أو منطقي مع نمطي سلسلة ثابتَين كنماذج فرعية. يتطابق النمط إذا كان
block.type
يتطابق مع أي من الأنماط الفرعيةp
أوcheckbox
.
- الحالة الأخيرة هي نمط حرف بدل،
_
. تطابق أحرف البدل في حالات التبديل كل شيء آخر. وتتصرف هذه العبارات بالطريقة نفسها التي تتصرف بها عباراتdefault
، والتي لا تزال مسموحًا بها في عبارات التبديل (فهي أكثر تفصيلاً).
يمكن استخدام أنماط أحرف البدل في أي مكان يُسمح فيه باستخدام نمط، على سبيل المثال، في نمط تعريف متغيّر: var (title, _) = document.metadata;
في هذا السياق، لا ترتبط أحرف البدل بأي متغيّر. ويتم تجاهل الحقل الثاني.
في القسم التالي، ستتعرّف على المزيد من ميزات التبديل بعد عرض عناصر Block
.
عرض محتوى المستند
أنشئ متغيّرًا محليًا يحتوي على قائمة عناصر Block
من خلال استدعاء getBlocks()
في طريقة build
الخاصة بالتطبيق المصغّر DocumentScreen
.
- استبدِل طريقة
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()
.
- شغِّل التطبيق، ومن المفترض أن تظهر لك الكتل على الشاشة:
10. استخدام تعبيرات التبديل
تضيف الأنماط الكثير من الإمكانات إلى switch
وcase
. ولإتاحة استخدامها في المزيد من الأماكن، تتضمّن Dart تعبيرات التبديل. يمكن أن تقدّم سلسلة من الحالات قيمة مباشرةً إلى عبارة تحديد متغيّر أو عبارة عرض.
تحويل عبارة التبديل إلى تعبير تبديل
يقدّم محلّل Dart إرشادات لمساعدتك في إجراء تغييرات على الرمز.
- حرِّك المؤشر إلى عبارة التبديل من القسم السابق.
- انقر على مصباح الإضاءة لعرض الاقتراحات المتاحة.
- اختَر ميزة المساعدة التحويل إلى تعبير تبديل.
يظهر الإصدار الجديد من هذا الرمز على النحو التالي:
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.
- ولا تُضيف هذه القواعد شرطًا إلى نمط إلا بعد مطابقته.
- إذا كانت قيمة عبارة الفحص هي خطأ، يتم تفنيد النمط بأكمله، ويستمر التنفيذ في الحالة التالية.
إضافة التاريخ بالتنسيق الجديد إلى واجهة المستخدم
- أخيرًا، عدِّل طريقة
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]);
},
),
),
],
),
);
}
}
- إعادة التحميل الفوري للاطّلاع على التغييرات في تطبيقك:
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
هي مُعدِّل فئة، ما يعني أنّه يمكنك توسيع نطاق هذه الفئة أو تنفيذها في المكتبة نفسها فقط. بما أنّ المحلّل يعرف الأنواع الفرعية لهذه الفئة، يُبلغ عن خطأ إذا تعذّر على أحد المفاتيح تغطية أحدها ولم يكن شاملاً.
استخدام تعبير تبديل لعرض التطبيقات المصغّرة
- عدِّل فئة
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
، بدلاً من عبارة الإرجاع المنفصلة التي كانت مطلوبة من قبل.
- يمكنك إعادة التحميل السريع للاطّلاع على بيانات JSON الخاصة بمربّع الاختيار التي يتم عرضها للمرة الأولى:
13. تهانينا
لقد جرّبت بنجاح الأنماط والسجلّات وعناصر التحويل والحالات المحسّنة والفئات المُغلقة. لقد تناولت الكثير من المعلومات، ولكنك لم تشرح هذه الميزات بشكل كامل. لمزيد من المعلومات عن الأنماط، يُرجى الاطّلاع على مواصفات الميزة.
إنّ أنواع الأنماط المختلفة والسياقات المختلفة التي يمكن أن تظهر فيها، بالإضافة إلى إمكانية تداخل الأنماط الفرعية، تجعل الاحتمالات في السلوك تبدو لا حصر لها. ولكن يسهل الاطّلاع عليها.
يمكنك تخيل جميع أنواع طرق عرض المحتوى في Flutter باستخدام الأنماط. باستخدام الأنماط، يمكنك استخراج البيانات بأمان لإنشاء واجهة المستخدم في بضعة أسطر من الرموز البرمجية.
الخطوة التالية
- اطّلِع على المستندات حول الأنماط والسجلّات وحالات التبديل المحسّنة وعوامل تعديل الفئات في قسم اللغة من مستندات Dart.
المستندات المرجعية
يمكنك الاطّلاع على نموذج الرمز البرمجي الكامل، خطوة بخطوة، في مستودع flutter/codelabs
.
للاطّلاع على مواصفات تفصيلية لكل ميزة جديدة، يمكنك الاطّلاع على مستندات التصميم الأصلية: