Погрузитесь в закономерности и рекорды Дартса

Погрузитесь в шаблоны и записи Dart

О практической работе

subjectПоследнее обновление: июн. 3, 2025
account_circleАвторы: John Ryan and Marya Belanger

1. Введение

Dart 3 вводит шаблоны в язык, новую крупную категорию грамматики. Помимо этого нового способа написания кода Dart, есть несколько других языковых улучшений, включая

  • записи для объединения данных разных типов,
  • модификаторы класса для управления доступом и
  • новые выражения switch и операторы if-case .

Эти функции расширяют ваши возможности при написании кода Dart. В этой кодовой лаборатории вы узнаете, как использовать их, чтобы сделать ваш код более компактным, оптимизированным и гибким.

Эта кодовая лаборатория предполагает, что вы немного знакомы с Flutter и Dart. Если вы чувствуете, что немного заржавели, рассмотрите возможность освежить основы с помощью следующих ресурсов:

Что вы построите

Эта кодовая лаборатория создает приложение, которое отображает документ JSON во Flutter. Приложение имитирует JSON, поступающий из внешнего источника. JSON содержит данные документа, такие как дата изменения, название, заголовки и абзацы. Вы пишете код, чтобы аккуратно упаковать данные в записи, чтобы их можно было передавать и распаковывать везде, где это необходимо вашим виджетам Flutter.

Затем вы используете шаблоны для построения соответствующего виджета, когда значение соответствует этому шаблону. Вы также видите, как использовать шаблоны для деструктуризации данных в локальные переменные.

Последнее приложение, которое вы создадите в этой лабораторной работе, — это документ с заголовком, датой последнего изменения, заголовками и абзацами.

Чему вы научитесь

  • Как создать запись, которая хранит несколько значений разных типов.
  • Как вернуть несколько значений из функции с помощью записи.
  • Как использовать шаблоны для сопоставления, проверки и деструктуризации данных из записей и других объектов.
  • Как привязать сопоставленные с шаблоном значения к новым или существующим переменным.
  • Как использовать новые возможности оператора switch, выражения switch и операторы if-case.
  • Как воспользоваться проверкой полноты , чтобы гарантировать обработку каждого случая в операторе switch или выражении switch.

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.
code patterns_codelab

VS Code отображает созданный проект

Установите минимальную версию 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(),
     
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. Чтобы убедиться, что все работает гладко, запустите приложение на хост-компьютере, нажав «Запустить и отладить» :

Кнопка «Запустить и отладить»

  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 . Записи — это легкий и простой способ вернуть несколько значений из одного вызова функции и назначить их переменной.

Чтобы получить доступ к отдельным полям, содержащимся в этой записи, можно использовать встроенный синтаксис геттера записи.

  • Чтобы получить позиционное поле (поле без имени, например 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 . Тело case выполняется только в том случае, если шаблон case соответствует данным в _json . Это соответствие выполняет те же проверки, которые вы написали в первой версии metadata для проверки входящего JSON. Этот код проверяет следующее:

  • _json — это тип карты.
  • _json содержит ключ metadata .
  • _json не является пустым.
  • _json['metadata'] также является типом карты.
  • _json['metadata'] содержит title ключей и modified .
  • title и localModified являются строками и не равны NULL.

Если значение не совпадает, шаблон опровергает (отказывается продолжать выполнение) и переходит к пункту 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() для преобразования).

Литерал списка содержит коллекцию для заполнения нового списка объектами 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. В первом операторе case используется константный шаблон строки . Шаблон совпадает, если block.type равен константному значению h1 .
  2. Второй оператор case использует шаблон логического ИЛИ с двумя шаблонами константных строк в качестве своих подшаблонов. Шаблон совпадает, если 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. Запустите приложение, и вы увидите на экране блоки:

Приложение отображает содержимое раздела «блоки» данных JSON.

10. Используйте выражения-переключатели

Шаблоны добавляют много возможностей к switch и case . Чтобы сделать их пригодными для использования в большем количестве мест, в Dart есть switch выражения . Серия case может предоставить значение непосредственно для присваивания переменной или оператора return.

Преобразовать оператор switch в выражение switch

Анализатор Dart поможет вам внести изменения в код.

  1. Переместите курсор на оператор switch из предыдущего раздела.
  2. Нажмите на лампочку, чтобы просмотреть доступные ассистенты.
  3. Выберите помощник по выражению «Преобразовать для переключения» .

Помощь «преобразовать в выражение-переключатель» доступна в 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),
    );
  }
}

Выражение switch похоже на оператор switch, но оно исключает ключевое слово case и использует => для отделения шаблона от тела 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',
  };
}

Этот метод возвращает выражение switch, которое включает difference значений, объект Duration . Он представляет собой промежуток времени между today ​​и modified значением из данных JSON.

Каждый случай выражения switch использует шаблон объекта, который сопоставляется путем вызова геттеров для свойств объекта inDays и isNegative . Синтаксис выглядит так, как будто он создает объект Duration, но на самом деле он обращается к полям в объекте difference .

В первых трех случаях используются константные подшаблоны 0 , 1 и -1 для сопоставления свойства объекта inDays и возврата соответствующей строки.

Последние два случая охватывают периоды времени, выходящие за рамки сегодняшнего, вчерашнего и завтрашнего дня:

  • Если свойство isNegative соответствует шаблону логической константы true , что означает, что дата изменения была в прошлом, отображается значение days ago .
  • Если в этом случае разница не улавливается, то продолжительность должна быть положительным числом дней (нет необходимости явно проверять с помощью 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',
  };
}

В этом кодексе вводятся защитные положения :

  • В защитном предложении после шаблона case используется ключевое слово 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. Горячая перезагрузка, чтобы увидеть изменения в вашем приложении:

Приложение, которое отображает строку «Последнее изменение: 2 недели назад» с помощью функции formatDate().

12. Запечатайте класс для исчерпывающего переключения

Обратите внимание, что вы не использовали подстановочный знак или случай по умолчанию в конце последнего переключения. Хотя хорошей практикой является всегда включать случай для значений, которые могут не сработать, это нормально в простом примере, как этот, поскольку вы знаете, что определенные вами случаи учитывают все возможные значения, которые может потенциально принять inDays .

Когда обрабатывается каждый случай в switch, это называется исчерпывающим switch . Например, переключение на тип bool является исчерпывающим, когда у него есть случаи для true и false . Переключение на тип enum также является исчерпывающим, когда есть случаи для каждого из значений enum, поскольку enum представляют фиксированное количество константных значений .

Dart 3 расширил проверку полноты для объектов и иерархий классов с новым модификатором класса sealed . Реорганизуйте класс Block в sealed superclass.

Создайте подклассы

  • В 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-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-данные флажка, отрисованные в первый раз:

Приложение, отображающее флажок «Изучить Dart 3»

13. Поздравления

Вы успешно экспериментировали с шаблонами, записями, улучшенным switch и case и запечатанными классами. Вы охватили много информации, но только едва коснулись поверхности этих функций. Для получения дополнительной информации о шаблонах см. спецификацию функций .

Различные типы паттернов, различные контексты, в которых они могут появляться, и потенциальное вложение подпаттернов делают возможности поведения, казалось бы, бесконечными . Но их легко увидеть.

Вы можете представить себе всевозможные способы отображения контента во Flutter с использованием шаблонов. Используя шаблоны, вы можете безопасно извлекать данные для построения своего пользовательского интерфейса в несколько строк кода.

Что дальше?

  • Ознакомьтесь с документацией по шаблонам, записям, расширенным переключателям и случаям, а также модификаторам классов в разделе «Язык» документации Dart.

Справочные документы

Полный пример кода, шаг за шагом, смотрите в репозитории flutter/codelabs .

Подробные спецификации каждой новой функции можно найти в исходной проектной документации: