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

1. Введение

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

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

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

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

Что ты построишь

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

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

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

Что вы узнаете

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

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, показывающий проект, созданный с помощью команды flutter create.

Установите минимальную версию SDK

  • Установите ограничение версии SDK для вашего проекта в зависимости от Dart 3 или более поздней версии.

pubspec.yaml

environment:
  sdk: ^3.0.0

4. Настройте проект

На этом этапе вы создаете или обновляете два файла Dart:

  • Файл main.dart , содержащий виджеты для приложения, и
  • Файл data.dart , содержащий данные приложения.

Вы продолжите изменять оба этих файла на последующих шагах.

Определите данные для приложения

  • Создайте новый файл lib/data.dart и добавьте в него следующий код:

библиотека/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 следующим кодом:

библиотека/main.dart

import 'package:flutter/material.dart';

import 'data.dart';

void main() {
  runApp(const DocumentApp());
}

class DocumentApp extends StatelessWidget {
  const DocumentApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(useMaterial3: true),
      home: DocumentScreen(
        document: Document(),
      ),
    );
  }
}

class DocumentScreen extends StatelessWidget {
  final Document document;

  const DocumentScreen({
    required this.document,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Title goes here'),
      ),
      body: const Column(
        children: [
          Center(
            child: Text('Body goes here'),
          ),
        ],
      ),
    );
  }
}

Вы добавили в приложение следующие два виджета:

  • DocumentApp устанавливает последнюю версию Material Design для оформления пользовательского интерфейса.
  • DocumentScreen обеспечивает визуальный макет страницы с помощью виджета Scaffold .
  1. Чтобы убедиться, что все работает гладко, запустите приложение на своем хост-компьютере, нажав «Запустить и отладить» :

Изображение кнопки «Запуск и отладка», доступной в разделе «Запуск и отладка» панели действий слева.

  1. По умолчанию Flutter выбирает любую доступную целевую платформу. Чтобы изменить целевую платформу, выберите текущую платформу в строке состояния:

Скриншот селектора целевой платформы в VS Code.

Вы должны увидеть пустой фрейм с элементами title и body , определенными в виджете DocumentScreen :

Скриншот приложения, созданного на этом этапе.

5. Создание и возврат записей

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

Создать и вернуть запись

  • В data.dart добавьте в класс Document новый метод получения с именем metadata , который возвращает запись:

библиотека/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 , чтобы вы могли получить свою запись и получить доступ к ее значениям:

библиотека/main.dart

class DocumentScreen extends StatelessWidget {
  final Document document;

  const DocumentScreen({
    required this.document,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    final metadataRecord = document.metadata;              // Add this line.

    return Scaffold(
      appBar: AppBar(
        title: Text(metadataRecord.$1),                    // Modify this line,
      ),
      body: Column(
        children: [
          Center(
            child: Text(
              'Last modified ${metadataRecord.modified}',  // And this one.
            ),
          ),
        ],
      ),
    );
  }
}

Метод получения metadata возвращает запись, которая присваивается локальной переменной metadataRecord . Записи — это легкий и простой способ вернуть несколько значений из одного вызова функции и назначить их переменной.

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

  • Чтобы получить позиционное поле (поле без имени, например title ), используйте метод получения $<num> для записи. Это возвращает только безымянные поля.
  • Именованные поля, такие как modified не имеют позиционного метода получения, поэтому вы можете использовать его имя напрямую, например, metadataRecord.modified .

Чтобы определить имя метода получения для позиционного поля, начните с $1 и пропускайте именованные поля. Например:

var record = (named: 'v', 'y', named2: 'x', 'z');
print(record.$1);                               // prints y
print(record.$2);                               // prints z
  1. Горячая перезагрузка, чтобы увидеть значения JSON, отображаемые в приложении. Плагин VS Code Dart выполняет горячую перезагрузку каждый раз, когда вы сохраняете файл.

Скриншот приложения, на котором отображается название и дата изменения.

Вы можете видеть, что каждое поле фактически сохранило свой тип.

  • Метод Text() принимает строку в качестве первого аргумента.
  • modified поле представляет собой DateTime и преобразуется в String с помощью интерполяции строк .

Другой типобезопасный способ возврата различных типов данных — определение класса, который является более подробным.

6. Сопоставление и деструктуризация с помощью шаблонов

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

Шаблон представляет собой структуру, которая может принимать одно или несколько значений, например схему. Шаблоны сравниваются с фактическими значениями, чтобы определить, совпадают ли они.

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

Разбить запись на локальные переменные

  1. Рефакторинг метода build DocumentScreen для вызова metadata и использования его для инициализации объявления переменной шаблона :

библиотека/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 следующим образом.

библиотека/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 :

библиотека/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 этим кодом:

библиотека/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 являются строками и не имеют значения NULL.

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

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

8. Подготовьте приложение для дополнительных шаблонов.

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

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

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

  • Добавьте новый класс Block в data.dart , который используется для чтения и хранения данных для одного из блоков данных JSON.

библиотека/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 с шаблоном карты, который вы видели раньше.

Обратите внимание, что json соответствует шаблону карты, хотя один из ключей, checked , не учитывается в шаблоне. Шаблоны карт игнорируют любые записи в объекте карты, которые явно не учтены в шаблоне.

Вернуть список объектов Block

  • Затем добавьте новую функцию getBlocks() в класс Document . getBlocks() анализирует JSON на экземпляры класса Block и возвращает список блоков для отображения в вашем пользовательском интерфейсе:

библиотека/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 — это лишь одно из усовершенствований управления структурами потока, которые идут в комплекте с шаблонами. Теперь вы примените свои знания об опровержимых закономерностях для переключения утверждений.

Управляйте тем, что отображается, используя шаблоны с операторами переключения.

  • В main.dart создайте новый виджет BlockWidget , который определяет стиль каждого блока на основе его поля type .

библиотека/main.dart

class BlockWidget extends StatelessWidget {
  final Block block;

  const BlockWidget({
    required this.block,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    TextStyle? textStyle;
    switch (block.type) {
      case 'h1':
        textStyle = Theme.of(context).textTheme.displayMedium;
      case 'p' || 'checkbox':
        textStyle = Theme.of(context).textTheme.bodyMedium;
      case _:
        textStyle = Theme.of(context).textTheme.bodySmall;
    }

    return Container(
      margin: const EdgeInsets.all(8),
      child: Text(
        block.text,
        style: textStyle,
      ),
    );
  }
}

Оператор переключения в методе build включает поле type block объекта.

  1. Первый оператор case использует шаблон постоянной строки . Шаблон соответствует, если block.type равен постоянному значению h1 .
  2. Второй оператор case использует шаблон «логическое ИЛИ» с двумя шаблонами константных строк в качестве подшаблонов. Шаблон соответствует, если block.type соответствует любому из подшаблонов p или checkbox .
  1. Последний случай — это шаблон подстановочных знаков _ . Подстановочные знаки в случаях переключателей соответствуют всему остальному. Они ведут себя так же, как и предложения default , которые по-прежнему разрешены в операторах переключения (только они немного более многословны).

Шаблоны подстановочных знаков можно использовать везде, где это разрешено, например в шаблоне объявления переменных: var (title, _) = document.metadata;

В этом контексте подстановочный знак не привязывает никакую переменную. Он отбрасывает второе поле.

В следующем разделе вы узнаете о дополнительных функциях переключения после отображения объектов Block .

Отображение содержимого документа

Создайте локальную переменную, содержащую список объектов Block , вызвав getBlocks() в методе build виджета DocumentScreen .

  1. Замените существующий метод build в DocumentationScreen этой версией:

библиотека/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 есть переключаемые выражения . В ряде случаев значение может быть передано непосредственно в оператор присваивания переменной или в оператор возврата.

Преобразование оператора переключателя в выражение переключателя

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

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

Снимок экрана помощника «преобразовать в выражение переключения», доступного в VS Code.

Новая версия этого кода выглядит так:

библиотека/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 :

библиотека/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 дней, чтобы пользовательский интерфейс мог отображать их в виде недель :

библиотека/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',
  };
}

Этот код вводит защитные положения :

  • Предложение Guard использует ключевое слово when после шаблона регистра.
  • Их можно использовать в if-cases, операторах переключения и выражениях переключения.
  • Они добавляют условие к шаблону только после его сопоставления .
  • Если предложение Guard оценивается как ложное, весь шаблон опровергается , и выполнение переходит к следующему случаю.

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

  1. Наконец, обновите метод build в DocumentScreen , чтобы использовать функцию formatDate :

библиотека/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 .

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

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

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

  • В data.dart создайте три новых класса — HeaderBlock , ParagraphBlock и CheckboxBlock — которые расширяют Block :

библиотека/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 как выражение переключателя, которое возвращает подкласс, соответствующий type , указанному в JSON:

библиотека/data.dart

sealed class Block {
  Block();

  factory Block.fromJson(Map<String, Object?> json) {
    return switch (json) {
      {'type': 'h1', 'text': String text} => HeaderBlock(text),
      {'type': 'p', 'text': String text} => ParagraphBlock(text),
      {'type': 'checkbox', 'text': String text, 'checked': bool checked} =>
        CheckboxBlock(text, checked),
      _ => throw const FormatException('Unexpected JSON format'),
    };
  }
}

Ключевое слово sealed — это модификатор класса , который означает, что вы можете расширить или реализовать этот класс только в той же библиотеке . Поскольку анализатору известны подтипы этого класса, он сообщает об ошибке, если переключатель не охватывает один из них и не является исчерпывающим.

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

  1. Обновите класс BlockWidget в main.dart , добавив выражение переключателя, которое использует шаблоны объектов для каждого случая:

библиотека/main.dart

class BlockWidget extends StatelessWidget {
  final Block block;

  const BlockWidget({
    required this.block,
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.all(8),
      child: switch (block) {
        HeaderBlock(:final text) => Text(
            text,
            style: Theme.of(context).textTheme.displayMedium,
          ),
        ParagraphBlock(:final text) => Text(text),
        CheckboxBlock(:final text, :final isChecked) => Row(
            children: [
              Checkbox(value: isChecked, onChanged: (_) {}),
              Text(text),
            ],
          ),
      },
    );
  }
}

В вашей первой версии BlockWidget вы включили поле объекта Block для возврата TextStyle . Теперь вы переключаете экземпляр самого объекта Block и сопоставляете его с шаблонами объектов , которые представляют его подклассы, извлекая при этом свойства объекта.

Анализатор Dart может проверить, что каждый подкласс обрабатывается в выражении переключателя, поскольку вы сделали Block закрытым классом.

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

  1. Горячая перезагрузка, чтобы увидеть данные JSON флажка, отображаемые в первый раз:

Скриншот приложения с флажком «Изучать дартс 3».

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

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

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

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

Что дальше?

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

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

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

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