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

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 ), используйте геттер. в записи. Возвращаются только неименованные поля.
  • У именованных полей, таких как 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 поле представляет собой объект типа 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 — это тип Map.
  • _json содержит ключ metadata .
  • _json не равен null.
  • _json['metadata'] также является типом Map.
  • _json['metadata'] содержит ключи title и modified .
  • title и localModified это строки, и они не равны null.

Если значение не совпадает, шаблон опровергает его (отказывается продолжать выполнение) и переходит к блоку else . Если совпадение успешно, шаблон деструктурирует значения title и modified из карты и связывает их с новыми локальными переменными.

Полный список шаблонов см. в таблице в разделе «Шаблоны» спецификации функции.

8. Подготовьте приложение к обработке большего количества шаблонов.

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

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

Создайте класс, который будет хранить данные.

  • Добавьте в файл data.dart новый класс Block , который будет использоваться для чтения и сохранения данных для одного из блоков в 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 с шаблоном map, который вы уже видели ранее.

Вы увидите, что данные 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.

Управляйте отображаемым контентом, используя шаблоны с помощью операторов 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.

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

Преобразуйте оператор switch в выражение switch.

Анализатор Dart предоставляет вспомогательные средства , которые помогут вам внести изменения в ваш код.

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

В VS Code доступна функция «Преобразовать в выражение switch».

Новая версия этого кода выглядит следующим образом:

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 , а для отделения шаблона от тела оператора используется оператор => . В отличие от операторов 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 , что означает, что дата изменения была в прошлом, отображается значение "дней назад" .
  • Если в этом случае разница не обнаруживается, то duration должно быть положительным числом дней (нет необходимости явно проверять это с помощью 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 используется после шаблона case.
  • Их можно использовать в условных операторах if-case, операторах switch и выражениях switch.
  • Они добавляют условие к шаблону только после того, как он совпал с заданным условием .
  • Если условие защиты принимает значение false, вся схема опровергается , и выполнение переходит к следующему случаю.

Добавьте отформатированную дату в пользовательский интерфейс.

  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. Выделить класс для исчерпывающего переключения.

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

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

В Dart 3 проверка полноты проверки была расширена на объекты и иерархии классов с помощью нового модификатора класса sealed . Преобразуйте ваш класс Block в суперкласс sealed.

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

  • В 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 в выражение 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 не охватывает ни один из них и не является исчерпывающим.

Для отображения виджетов используйте оператор 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 для флажка, обновите страницу:

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

13. Поздравляем!

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

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

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

Что дальше?

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

Справочная документация

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

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