Ваше первое приложение Flutter

1. Введение

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

Приложение генерирует крутые имена, такие как «newstay», «lightstream», «mainbrake» или «graypine». Пользователь может запросить следующее имя, добавить в избранное текущее имя и просмотреть список избранных имен на отдельной странице. Приложение реагирует на разные размеры экрана.

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

  • Основы работы Flutter
  • Создание макетов во Flutter
  • Связывание действий пользователя (например, нажатий кнопок) с поведением приложения.
  • Поддержание порядка в коде Flutter
  • Сделайте ваше приложение адаптивным (для разных экранов)
  • Достижение единообразного внешнего вида вашего приложения.

Вы начнете с простого каркаса, чтобы сразу перейти к интересным частям.

e9c6b402cd8003fd.png

А вот Филип проведет вас через всю кодовую лабораторию!

Нажмите «Далее», чтобы начать лабораторию.

2. Настройте среду Flutter.

Редактор

Чтобы сделать эту лабораторную работу максимально простой, мы предполагаем, что вы будете использовать Visual Studio Code (VS Code) в качестве среды разработки. Это бесплатно и работает на всех основных платформах.

Конечно, можно использовать любой редактор, который вам нравится: Android Studio, другие IDE IntelliJ, Emacs, Vim или Notepad++. Все они работают с Flutter.

Мы рекомендуем использовать VS Code для этой лаборатории кода, поскольку в инструкциях по умолчанию используются сочетания клавиш, специфичные для VS Code. Легче сказать что-то вроде «нажмите здесь» или «нажмите эту клавишу», а не что-то вроде «выполните соответствующее действие в редакторе, чтобы сделать X».

228c71510a8e868.png

Выберите цель развития

Flutter — это мультиплатформенный набор инструментов. Ваше приложение может работать в любой из следующих операционных систем:

  • iOS
  • Андроид
  • Окна
  • macOS
  • Линукс
  • сеть

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

16695777c07f18e5.png

Например, предположим, что вы используете ноутбук с Windows для разработки приложения Flutter. Если вы выбираете Android в качестве цели разработки, вы обычно подключаете устройство Android к ноутбуку с Windows с помощью USB-кабеля, и ваше разрабатываемое приложение запускается на этом подключенном устройстве Android. Но вы также можете выбрать Windows в качестве цели разработки, что означает, что ваше разрабатываемое приложение работает как приложение Windows вместе с вашим редактором.

Может возникнуть соблазн выбрать Интернет в качестве цели разработки. Обратной стороной этого выбора является то, что вы теряете одну из самых полезных функций разработки Flutter: горячую перезагрузку с сохранением состояния. Flutter не может выполнять горячую перезагрузку веб-приложений.

Сделайте свой выбор сейчас. Помните: позже вы всегда сможете запустить свое приложение в других операционных системах. Просто наличие четкой цели развития делает следующий шаг более плавным.

Установить флаттер

Самые актуальные инструкции по установке Flutter SDK всегда находятся на docs.flutter.dev .

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

  1. Флаттер SDK
  2. Код Visual Studio с плагином Flutter
  3. Программное обеспечение, необходимое для выбранной вами цели разработки (например: Visual Studio для Windows или Xcode для macOS).

В следующем разделе вы создадите свой первый проект Flutter.

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

Часто задаваемые вопросы

3. Создайте проект

Создайте свой первый проект Flutter

Запустите Visual Studio Code и откройте палитру команд (с помощью F1 или Ctrl+Shift+P или Shift+Cmd+P ). Начните вводить «флаттер новый». Выберите команду Flutter: New Project .

Затем выберите «Приложение» , а затем папку, в которой нужно создать проект. Это может быть ваш домашний каталог или что-то вроде C:\src\ .

Наконец, назовите свой проект. Что-то вроде namer_app или my_awesome_namer .

260a7d97f9678005.png

Теперь Flutter создает папку вашего проекта, и VS Code открывает ее.

Теперь вы перезапишете содержимое трех файлов базовым шаблоном приложения.

Скопируйте и вставьте исходное приложение

На левой панели VS Code убедитесь, что выбран проводник , и откройте файл pubspec.yaml .

e2a5bab0be07f4f7.png

Замените содержимое этого файла следующим:

pubspec.yaml

name: namer_app
description: A new Flutter project.

publish_to: 'none' # Remove this line if you wish to publish to pub.dev

version: 0.0.1+1

environment:
  sdk: ^3.1.1

dependencies:
  flutter:
    sdk: flutter

  english_words: ^4.0.0
  provider: ^6.0.0

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^2.0.0

flutter:
  uses-material-design: true

Файл pubspec.yaml содержит базовую информацию о вашем приложении, такую ​​как его текущая версия, его зависимости и ресурсы, с которыми оно будет поставляться.

Затем откройте в проекте еще один файл конфигурации — analysis_options.yaml .

a781f218093be8e0.png

Замените его содержимое следующим:

Analysis_options.yaml

include: package:flutter_lints/flutter.yaml

linter:
  rules:
    avoid_print: false
    prefer_const_constructors_in_immutables: false
    prefer_const_constructors: false
    prefer_const_literals_to_create_immutables: false
    prefer_final_fields: false
    unnecessary_breaks: true
    use_key_in_widget_constructors: false

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

Наконец, откройте файл main.dart в каталоге lib/ .

e54c671c9bb4d23d.png

Замените содержимое этого файла следующим:

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

import 'package:english_words/english_words.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => MyAppState(),
      child: MaterialApp(
        title: 'Namer App',
        theme: ThemeData(
          useMaterial3: true,
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
        ),
        home: MyHomePage(),
      ),
    );
  }
}

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();

    return Scaffold(
      body: Column(
        children: [
          Text('A random idea:'),
          Text(appState.current.asLowerCase),
        ],
      ),
    );
  }
}

Эти 50 строк кода на данный момент составляют все приложение.

В следующем разделе запустите приложение в режиме отладки и начните разработку.

4. Добавьте кнопку

На этом шаге добавляется кнопка «Далее» для создания новой пары слов.

Запустите приложение

Сначала откройте lib/main.dart и убедитесь, что выбрано целевое устройство. В правом нижнем углу VS Code вы найдете кнопку, показывающую текущее целевое устройство. Нажмите, чтобы изменить его.

Пока lib/main.dart открыт, найдите «play». b0a5d0200af5985d.png кнопку в правом верхнем углу окна VS Code и щелкните ее.

Примерно через минуту ваше приложение запустится в режиме отладки. Это пока не так уж и много:

f96e7dfb0937d7f4.png

Первая горячая перезагрузка

В нижней части lib/main.dart добавьте что-нибудь к строке в первом объекте Text и сохраните файл (с помощью Ctrl+S или Cmd+S ). Например:

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

// ...

    return Scaffold(
      body: Column(
        children: [
          Text('A random AWESOME idea:'),  // ← Example change.
          Text(appState.current.asLowerCase),
        ],
      ),
    );

// ...

Обратите внимание, как приложение сразу меняется, но случайное слово остается прежним. Это знаменитая горячая перезагрузка Flutter с сохранением состояния в действии. Горячая перезагрузка запускается, когда вы сохраняете изменения в исходном файле.

Часто задаваемые вопросы

Добавление кнопки

Затем добавьте кнопку внизу Column , прямо под вторым экземпляром Text .

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

// ...

    return Scaffold(
      body: Column(
        children: [
          Text('A random AWESOME idea:'),
          Text(appState.current.asLowerCase),

          // ↓ Add this.
          ElevatedButton(
            onPressed: () {
              print('button pressed!');
            },
            child: Text('Next'),
          ),

        ],
      ),
    );

// ...

Когда вы сохраняете изменения, приложение снова обновляется: появляется кнопка, и когда вы нажимаете ее, консоль отладки в VS Code показывает нажатую кнопку! сообщение.

Ускоренный курс Flutter за 5 минут

Как бы ни было интересно наблюдать за консолью отладки , вам хочется, чтобы кнопка делала что-то более значимое. Однако прежде чем перейти к этому, взгляните повнимательнее на код в lib/main.dart , чтобы понять, как он работает.

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

// ...

void main() {
  runApp(MyApp());
}

// ...

В самом верху файла вы найдете функцию main() . В своей текущей форме он только сообщает Flutter о запуске приложения, определенного в MyApp .

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

// ...

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

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (context) => MyAppState(),
      child: MaterialApp(
        title: 'Namer App',
        theme: ThemeData(
          useMaterial3: true,
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
        ),
        home: MyHomePage(),
      ),
    );
  }
}

// ...

Класс MyApp расширяет StatelessWidget . Виджеты — это элементы, из которых вы создаете каждое приложение Flutter. Как видите, даже само приложение представляет собой виджет.

Код в MyApp настраивает все приложение. Он создает состояние всего приложения (подробнее об этом позже), дает приложению имя, определяет визуальную тему и устанавливает «домашний» виджет — отправную точку вашего приложения.

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

// ...

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();
}

// ...

Далее класс MyAppState определяет… ну… состояние приложения. Это ваш первый опыт работы с Flutter, поэтому эта лаборатория кода будет простой и целенаправленной. Во Flutter существует множество мощных способов управления состоянием приложения. Один из самых простых для объяснения — это ChangeNotifier , подход, использованный в этом приложении.

  • MyAppState определяет данные, необходимые приложению для работы. На данный момент он содержит только одну переменную с текущей парой случайных слов. Вы добавите это позже.
  • Класс состояния расширяет ChangeNotifier , что означает, что он может уведомлять других о своих собственных изменениях . Например, если текущая пара слов изменится, некоторые виджеты в приложении должны об этом узнать.
  • Состояние создается и предоставляется всему приложению с помощью ChangeNotifierProvider (см. код выше в MyApp ). Это позволяет любому виджету в приложении получать информацию о состоянии. d9b6ecac5494a6ff.png

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

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {           // ← 1
    var appState = context.watch<MyAppState>();  // ← 2

    return Scaffold(                             // ← 3
      body: Column(                              // ← 4
        children: [
          Text('A random AWESOME idea:'),        // ← 5
          Text(appState.current.asLowerCase),    // ← 6
          ElevatedButton(
            onPressed: () {
              print('button pressed!');
            },
            child: Text('Next'),
          ),
        ],                                       // ← 7
      ),
    );
  }
}

// ...

Наконец, есть MyHomePage , виджет, который вы уже изменили. Каждая пронумерованная строка ниже соответствует комментарию к номеру строки в приведенном выше коде:

  1. Каждый виджет определяет метод build() , который автоматически вызывается каждый раз, когда обстоятельства виджета меняются, чтобы виджет всегда был актуальным.
  2. MyHomePage отслеживает изменения текущего состояния приложения с помощью метода watch .
  3. Каждый метод build должен возвращать виджет или (чаще) вложенное дерево виджетов. В данном случае виджет верхнего уровня — Scaffold . В этой лаборатории кода вы не будете работать с Scaffold , но это полезный виджет, который можно найти в подавляющем большинстве реальных приложений Flutter.
  4. Column — один из самых простых виджетов макета во Flutter. Он берет любое количество детей и помещает их в столбец сверху вниз. По умолчанию столбец визуально размещает дочерние элементы вверху. Вскоре вы измените это так, чтобы столбец располагался по центру.
  5. Вы изменили этот виджет Text на первом шаге.
  6. Этот второй виджет Text принимает appState и обращается к единственному члену этого класса, current ( WordPair ). WordPair предоставляет несколько полезных методов получения, таких как asPascalCase или asSnakeCase . Здесь мы используем asLowerCase , но вы можете изменить это сейчас, если предпочитаете один из альтернатив.
  7. Обратите внимание, как код Flutter активно использует конечные запятые. Эта конкретная запятая не обязательно должна быть здесь, потому что children — это последний (и единственный ) член этого конкретного списка параметров Column . Тем не менее, использование завершающих запятых, как правило, является хорошей идеей: они упрощают добавление дополнительных элементов, а также служат подсказкой для автоформатера Dart о необходимости поместить туда новую строку. Дополнительные сведения см. в разделе Форматирование кода .

Далее вы подключите кнопку к состоянию.

Ваше первое поведение

Прокрутите страницу до MyAppState и добавьте метод getNext .

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

// ...

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();

  // ↓ Add this.
  void getNext() {
    current = WordPair.random();
    notifyListeners();
  }
}

// ...

Новый метод getNext() переназначает current с помощью новой случайной WordPair . Он также вызывает notifyListeners() (метод ChangeNotifier) , который гарантирует, что каждый, кто смотрит MyAppState будет уведомлен.

Остается только вызвать метод getNext из обратного вызова кнопки.

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

// ...

    ElevatedButton(
      onPressed: () {
        appState.getNext();  // ← This instead of print().
      },
      child: Text('Next'),
    ),

// ...

Сохраните и попробуйте приложение прямо сейчас. Он должен генерировать новую случайную пару слов каждый раз, когда вы нажимаете кнопку «Далее» .

В следующем разделе вы улучшите пользовательский интерфейс.

5. Сделайте приложение красивее

Вот как приложение выглядит на данный момент.

3dd8a9d8653bdc56.png

Не здорово. Центральная часть приложения — случайно сгенерированная пара слов — должна быть более заметной. В конце концов, это главная причина, по которой наши пользователи используют это приложение! Кроме того, содержимое приложения странно смещено по центру, и все приложение скучно черно-белое.

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

2bbee054d81a3127.png

Извлечь виджет

Строка, отвечающая за отображение текущей пары слов, теперь выглядит так: Text(appState.current.asLowerCase) . Чтобы превратить ее во что-то более сложное, рекомендуется выделить эту строку в отдельный виджет. Наличие отдельных виджетов для отдельных логических частей вашего пользовательского интерфейса — важный способ управления сложностью во Flutter.

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

По этой причине перепишите виджет MyHomePage следующим образом:

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

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();  
    var pair = appState.current;                 // ← Add this.

    return Scaffold(
      body: Column(
        children: [
          Text('A random AWESOME idea:'),
          Text(pair.asLowerCase),                // ← Change to this.
          ElevatedButton(
            onPressed: () {
              appState.getNext();
            },
            child: Text('Next'),
          ),
        ],
      ),
    );
  }
}

// ...

Хороший. Виджет Text больше не ссылается на весь appState .

Теперь вызовите меню «Рефакторинг» . В VS Code это можно сделать одним из двух способов:

  1. Щелкните правой кнопкой мыши фрагмент кода, который вы хотите рефакторить (в данном случае Text ), и выберите «Рефакторинг...» в раскрывающемся меню.

ИЛИ

  1. Переместите курсор на фрагмент кода, который вы хотите реорганизовать (в данном случае Text ), и нажмите Ctrl+. (Win/Linux) или Cmd+. (Мак).

В меню «Рефакторинг» выберите «Извлечь виджет» . Назначьте имя, например BigCard , и нажмите Enter .

Это автоматически создаст новый класс BigCard в конце текущего файла. Класс выглядит примерно так:

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

// ...

class BigCard extends StatelessWidget {
  const BigCard({
    super.key,
    required this.pair,
  });

  final WordPair pair;

  @override
  Widget build(BuildContext context) {
    return Text(pair.asLowerCase);
  }
}

// ...

Обратите внимание, как приложение продолжает работать даже после этого рефакторинга.

Добавить карту

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

Найдите класс BigCard и метод build() внутри него. Как и раньше, вызовите меню «Рефакторинг» виджета Text . Однако на этот раз вы не собираетесь извлекать виджет.

Вместо этого выберите «Обтекание с отступом» . Это создаст новый родительский виджет вокруг виджета Text под названием Padding . После сохранения вы увидите, что у случайного слова уже есть больше места для передышки.

Увеличьте отступ со значения по умолчанию 8.0 . Например, используйте что-то вроде 20 для более просторного заполнения.

Далее поднимитесь на один уровень выше. Наведите курсор на виджет Padding , откройте меню «Рефакторинг» и выберите «Обтекание виджетом...» .

Это позволяет вам указать родительский виджет. Введите «Карта» и нажмите Enter .

Это обертывает виджет Padding и, следовательно, Text , виджетом Card .

6031adbc0a11e16b.png

Тема и стиль

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

Внесите следующие изменения в метод build() BigCard .

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

// ...

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);       // ← Add this.

    return Card(
      color: theme.colorScheme.primary,    // ← And also this.
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Text(pair.asLowerCase),
      ),
    );
  }

// ...

Эти две новые строки выполняют большую работу:

  • Сначала код запрашивает текущую тему приложения с помощью Theme.of(context) .
  • Затем код определяет цвет карты, который совпадает со свойством colorScheme темы. Цветовая схема содержит множество цветов, и primary является наиболее заметный, определяющий цвет приложения.

Карточка теперь окрашена в основной цвет приложения:

a136f7682c204ea1.png

Вы можете изменить этот цвет и цветовую схему всего приложения, прокрутив до MyApp и изменив там исходный цвет для ColorScheme .

Обратите внимание, как плавно анимируется цвет. Это называется неявной анимацией . Многие виджеты Flutter плавно интерполируют значения, так что пользовательский интерфейс не просто «перескакивает» между состояниями.

Приподнятая кнопка под карточкой также меняет цвет. В этом преимущество использования Theme для всего приложения, а не жесткого кодирования значений.

Текстовая тема

С карточкой все еще есть проблема: текст слишком мелкий, а его цвет плохо читается. Чтобы это исправить, внесите следующие изменения в метод build() BigCard .

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

// ...

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    // ↓ Add this.
    final style = theme.textTheme.displayMedium!.copyWith(
      color: theme.colorScheme.onPrimary,
    );

    return Card(
      color: theme.colorScheme.primary,
      child: Padding(
        padding: const EdgeInsets.all(20),
        // ↓ Change this line.
        child: Text(pair.asLowerCase, style: style),
      ),
    );
  }

// ...

Что стоит за этим изменением:

  • Используя theme.textTheme, вы получаете доступ к теме шрифта приложения. Этот класс включает такие члены, как bodyMedium (для стандартного текста среднего размера), caption (для подписей к изображениям) или headlineLarge (для больших заголовков).
  • Свойство displayMedium — это большой стиль, предназначенный для отображения текста. Слово « дисплей» здесь используется в типографском смысле, например, в «дисплейном шрифте» . В документации displayMedium говорится, что «стили отображения зарезервированы для короткого важного текста» — это именно наш вариант использования.
  • Свойство displayMedium темы теоретически может иметь значение null . Dart, язык программирования, на котором вы пишете это приложение, является нулевым, поэтому он не позволит вам вызывать методы объектов, которые потенциально имеют null . Однако в этом случае вы можете использовать ! оператор («оператор взрыва»), чтобы заверить Дарта, что вы знаете, что делаете. (В этом случае displayMedium определенно не равен нулю. Однако причина, по которой мы это знаем, выходит за рамки этой кодовой лаборатории.)
  • Вызов copyWith() в displayMedium возвращает копию стиля текста с определенными вами изменениями. В этом случае вы меняете только цвет текста.
  • Чтобы получить новый цвет, вы снова получаете доступ к теме приложения. Свойство onPrimary цветовой схемы определяет цвет, который хорошо подходит для использования в качестве основного цвета приложения.

Приложение теперь должно выглядеть примерно так:

2405e9342d28c193.png

Если вам так хочется, меняйте карту дальше. Вот несколько идей:

  • copyWith() позволяет вам изменить стиль текста гораздо больше, чем просто цвет. Чтобы получить полный список свойств, которые вы можете изменить, поместите курсор в круглые скобки copyWith() и нажмите Ctrl+Shift+Space (Win/Linux) или Cmd+Shift+Space (Mac).
  • Аналогичным образом вы можете изменить виджет Card . Например, вы можете увеличить тень карты, увеличив значение параметра elevation .
  • Попробуйте поэкспериментировать с цветами. Помимо theme.colorScheme.primary , есть еще .secondary , .surface и множество других. Все эти цвета имеют эквиваленты onPrimary .

Улучшение доступности

Flutter делает приложения доступными по умолчанию. Например, каждое приложение Flutter правильно отображает весь текст и интерактивные элементы в приложении для программ чтения с экрана, таких как TalkBack и VoiceOver.

d1fad7944fb890ea.png

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

Простое решение — заменить pair.asLowerCase на "${pair.first} ${pair.second}" . Последний использует интерполяцию строк для создания строки (например, "cheap head" ) из двух слов, содержащихся в pair . Использование двух отдельных слов вместо составного слова гарантирует, что программы чтения с экрана идентифицируют их правильно, и обеспечивает лучший опыт для пользователей с ослабленным зрением.

Однако вы можете сохранить визуальную простоту pair.asLowerCase . Используйте свойство semanticsLabel Text , чтобы переопределить визуальное содержимое текстового виджета семантическим содержимым, более подходящим для программ чтения с экрана:

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

// ...

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    final style = theme.textTheme.displayMedium!.copyWith(
      color: theme.colorScheme.onPrimary,
    );

    return Card(
      color: theme.colorScheme.primary,
      child: Padding(
        padding: const EdgeInsets.all(20),

        // ↓ Make the following change.
        child: Text(
          pair.asLowerCase,
          style: style,
          semanticsLabel: "${pair.first} ${pair.second}",
        ),
      ),
    );
  }

// ...

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

Центрировать пользовательский интерфейс

Теперь, когда случайная пара слов представлена ​​достаточно визуально, пришло время разместить ее в центре окна/экрана приложения.

Во-первых, помните, что BigCard является частью Column . По умолчанию столбцы помещают своих дочерних элементов наверх, но мы можем легко это изменить. Перейдите к методу build() MyHomePage и внесите следующее изменение:

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

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    return Scaffold(
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,  // ← Add this.
        children: [
          Text('A random AWESOME idea:'),
          BigCard(pair: pair),
          ElevatedButton(
            onPressed: () {
              appState.getNext();
            },
            child: Text('Next'),
          ),
        ],
      ),
    );
  }
}

// ...

Это центрирует детей внутри Column вдоль ее главной (вертикальной) оси.

b555d4c7f5000edf.png

Дочерние элементы уже центрированы по поперечной оси столбца (другими словами, они уже центрированы по горизонтали). Но сама Column не находится по центру внутри Scaffold . Мы можем проверить это с помощью Widget Inspector .

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

Вы можете просто центрировать сам столбец. Поместите курсор на Column , вызовите меню Refactor (с помощью Ctrl+. или Cmd+. ) и выберите Wrap with Center .

Приложение теперь должно выглядеть примерно так:

455688d93c30d154.png

Если вы хотите, вы можете настроить это еще немного.

  • Вы можете удалить виджет Text над BigCard . Можно утверждать, что описательный текст («Случайная УДИВИТЕЛЬНАЯ идея:») больше не нужен, поскольку пользовательский интерфейс имеет смысл даже без него. И так чище.
  • Вы также можете добавить виджет SizedBox(height: 10) между BigCard и ElevatedButton . Таким образом, между двумя виджетами будет немного больше разделения. Виджет SizedBox просто занимает место и сам по себе ничего не отображает. Обычно его используют для создания визуальных «пробелов».

С учетом необязательных изменений MyHomePage содержит следующий код:

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

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            BigCard(pair: pair),
            SizedBox(height: 10),
            ElevatedButton(
              onPressed: () {
                appState.getNext();
              },
              child: Text('Next'),
            ),
          ],
        ),
      ),
    );
  }
}

// ...

И приложение выглядит следующим образом:

3d53d2b071e2f372.png

В следующем разделе вы добавите возможность добавлять в избранное (или ставить лайки) сгенерированные слова.

6. Добавьте функциональность

Приложение работает и иногда даже выдает интересные пары слов. Но всякий раз, когда пользователь нажимает «Далее» , каждая пара слов исчезает навсегда. Было бы лучше иметь возможность «запоминать» лучшие предложения: например, кнопку «Мне нравится».

e6b01a8c90df8ffa.png

Добавляем бизнес-логику

Прокрутите страницу до MyAppState и добавьте следующий код:

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

// ...

class MyAppState extends ChangeNotifier {
  var current = WordPair.random();

  void getNext() {
    current = WordPair.random();
    notifyListeners();
  }

  // ↓ Add the code below.
  var favorites = <WordPair>[];

  void toggleFavorite() {
    if (favorites.contains(current)) {
      favorites.remove(current);
    } else {
      favorites.add(current);
    }
    notifyListeners();
  }
}

// ...

Изучите изменения:

  • Вы добавили в MyAppState новое свойство под названием favorites . Это свойство инициализируется пустым списком: [] .
  • Вы также указали, что список может содержать только пары слов: <WordPair>[] , используя дженерики . Это помогает сделать ваше приложение более надежным — Dart отказывается даже запускать ваше приложение, если вы попытаетесь добавить в него что-либо, кроме WordPair . В свою очередь, вы можете использовать список favorites , зная, что там никогда не могут скрываться нежелательные объекты (например, null ).
  • Вы также добавили новый метод toggleFavorite() , который либо удаляет текущую пару слов из списка избранного (если она уже там), либо добавляет ее (если ее там еще нет). В любом случае код вызывает notifyListeners(); после.

Добавьте кнопку

Разобравшись с «бизнес-логикой», пришло время снова поработать над пользовательским интерфейсом. Для размещения кнопки «Мне нравится» слева от кнопки «Далее» требуется Row . Виджет Row — это горизонтальный эквивалент Column , который вы видели ранее.

Сначала оберните существующую кнопку в Row . Перейдите к методу build() MyHomePage , поместите курсор на ElevatedButton , вызовите меню рефакторинга с помощью Ctrl+. или Cmd+. и выберите «Обтекание строкой» .

При сохранении вы заметите, что Row действует аналогично Column — по умолчанию она смещает дочерние элементы влево. ( Column переместил своих дочерних элементов вверх.) Чтобы исправить это, вы можете использовать тот же подход, что и раньше, но с mainAxisAlignment . Однако в дидактических целях (обучения) используйте mainAxisSize . Это говорит Row не занимать все доступное горизонтальное пространство.

Внесите следующее изменение:

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

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            BigCard(pair: pair),
            SizedBox(height: 10),
            Row(
              mainAxisSize: MainAxisSize.min,   // ← Add this.
              children: [
                ElevatedButton(
                  onPressed: () {
                    appState.getNext();
                  },
                  child: Text('Next'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

// ...

Пользовательский интерфейс вернулся туда, где он был раньше.

3d53d2b071e2f372.png

Затем добавьте кнопку «Нравится» и подключите ее к toggleFavorite() . Чтобы посложнее, сначала попробуйте сделать это самостоятельно, не глядя на блок кода ниже.

e6b01a8c90df8ffa.png

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

Также совершенно нормально терпеть неудачу — в конце концов, это ваш первый час с Flutter.

252f7c4a212c94d2.png

Вот один из способов добавить вторую кнопку в MyHomePage . На этот раз используйте конструктор ElevatedButton.icon() , чтобы создать кнопку со значком. А вверху метода build выберите соответствующий значок в зависимости от того, находится ли текущая пара слов уже в избранном. Также обратите внимание на использование SizedBox еще раз, чтобы две кнопки были немного раздвинуты.

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

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    // ↓ Add this.
    IconData icon;
    if (appState.favorites.contains(pair)) {
      icon = Icons.favorite;
    } else {
      icon = Icons.favorite_border;
    }

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            BigCard(pair: pair),
            SizedBox(height: 10),
            Row(
              mainAxisSize: MainAxisSize.min,
              children: [

                // ↓ And this.
                ElevatedButton.icon(
                  onPressed: () {
                    appState.toggleFavorite();
                  },
                  icon: Icon(icon),
                  label: Text('Like'),
                ),
                SizedBox(width: 10),

                ElevatedButton(
                  onPressed: () {
                    appState.getNext();
                  },
                  child: Text('Next'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

// ...

Приложение должно выглядеть следующим образом:

К сожалению, пользователь не может видеть избранное. Пришло время добавить в наше приложение целый отдельный экран. До встречи в следующем разделе!

7. Добавьте направляющую навигации.

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

f62c54f5401a187.png

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

Выделите все MyHomePage , удалите их и замените следующим кодом:

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

// ...

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(
        children: [
          SafeArea(
            child: NavigationRail(
              extended: false,
              destinations: [
                NavigationRailDestination(
                  icon: Icon(Icons.home),
                  label: Text('Home'),
                ),
                NavigationRailDestination(
                  icon: Icon(Icons.favorite),
                  label: Text('Favorites'),
                ),
              ],
              selectedIndex: 0,
              onDestinationSelected: (value) {
                print('selected: $value');
              },
            ),
          ),
          Expanded(
            child: Container(
              color: Theme.of(context).colorScheme.primaryContainer,
              child: GeneratorPage(),
            ),
          ),
        ],
      ),
    );
  }
}


class GeneratorPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();
    var pair = appState.current;

    IconData icon;
    if (appState.favorites.contains(pair)) {
      icon = Icons.favorite;
    } else {
      icon = Icons.favorite_border;
    }

    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          BigCard(pair: pair),
          SizedBox(height: 10),
          Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              ElevatedButton.icon(
                onPressed: () {
                  appState.toggleFavorite();
                },
                icon: Icon(icon),
                label: Text('Like'),
              ),
              SizedBox(width: 10),
              ElevatedButton(
                onPressed: () {
                  appState.getNext();
                },
                child: Text('Next'),
              ),
            ],
          ),
        ],
      ),
    );
  }
}

// ...

После сохранения вы увидите, что визуальная часть пользовательского интерфейса готова, но она не работает. Нажатие ♥︎ (сердце) на панели навигации ничего не дает.

388bc25fe198c54a.png

Изучите изменения.

  • Во-первых, обратите внимание, что все содержимое MyHomePage извлекается в новый виджет GeneratorPage . Единственная часть старого виджета MyHomePage , которая не была извлечена, — это Scaffold .
  • Новый MyHomePage содержит Row с двумя дочерними элементами. Первый виджет — SafeArea , а второй — Expanded виджет.
  • SafeArea гарантирует, что его дочерний элемент не будет закрыт аппаратным вырезом или строкой состояния. В этом приложении виджет обтекает NavigationRail , чтобы кнопки навигации не закрывались, например, строкой состояния мобильного устройства.
  • Вы можете изменить строку extended: false в NavigationRail на true . Рядом с значками отображаются метки. На следующем этапе вы узнаете, как сделать это автоматически, когда в приложении будет достаточно горизонтального пространства.
  • На навигационной панели есть два пункта назначения ( «Домой» и «Избранное» ) с соответствующими значками и метками. Он также определяет текущий selectedIndex . Выбранный индекс, равный нулю, выбирает первый пункт назначения, выбранный индекс, равный единице, выбирает второй пункт назначения и так далее. На данный момент он жестко запрограммирован на ноль.
  • Навигационная направляющая также определяет, что происходит, когда пользователь выбирает одно из пунктов назначения с помощью onDestinationSelected . Прямо сейчас приложение просто выводит запрошенное значение индекса с помощью print() .
  • Вторым дочерним элементом Row является виджет Expanded . Расширенные виджеты чрезвычайно полезны в строках и столбцах — они позволяют создавать макеты, в которых некоторые дочерние элементы занимают ровно столько места, сколько им необходимо ( в данном случае SafeArea ), а другие виджеты должны занимать как можно больше оставшегося места ( Expanded в данном случае). этот случай). Один из способов думать о Expanded виджетах — это то, что они «жадные». Если вы хотите лучше понять роль этого виджета, попробуйте обернуть виджет SafeArea другим Expanded . В результате макет выглядит примерно так:

6bbda6c1835a1ae.png

  • Два Expanded виджета разделили между собой все доступное горизонтальное пространство, хотя навигационной панели действительно требовался лишь небольшой кусочек слева.
  • Внутри виджета Expanded есть цветной Container , а внутри контейнера — GeneratorPage .

Виджеты без сохранения и сохранения состояния

До сих пор MyAppState покрывал все потребности вашего штата. Вот почему все виджеты, которые вы написали до сих пор, не имеют состояния. Они не содержат собственного изменяемого состояния. Ни один из виджетов не может измениться сам — они должны пройти через MyAppState .

Это скоро изменится.

Вам нужен какой-то способ сохранить значение selectedIndex навигационной направляющей. Вы также хотите иметь возможность изменять это значение в обратном вызове onDestinationSelected .

Вы можете добавить selectedIndex как еще одно свойство MyAppState . И это сработает. Но вы можете себе представить, что состояние приложения быстро вышло бы за рамки разумного, если бы каждый виджет сохранял в нем свои значения.

e52d9c0937cc0823.jpeg

Некоторое состояние относится только к одному виджету, поэтому оно должно оставаться с этим виджетом.

Введите StatefulWidget , тип виджета, который имеет State . Сначала преобразуйте MyHomePage в виджет с сохранением состояния.

Поместите курсор в первую строку MyHomePage (ту, которая начинается с class MyHomePage... ) и вызовите меню «Рефакторинг» , используя Ctrl+. или Cmd+. . Затем выберите «Преобразовать в StatefulWidget» .

IDE создаст для вас новый класс _MyHomePageState . Этот класс расширяет State и, следовательно, может управлять своими собственными значениями. (Он может измениться сам .) Также обратите внимание, что метод build из старого виджета без сохранения состояния переместился в _MyHomePageState (вместо того, чтобы оставаться в виджете). Он был перенесен дословно — внутри метода build ничего не изменилось. Теперь он просто живет где-то в другом месте.

setState

Новому виджету с состоянием необходимо отслеживать только одну переменную: selectedIndex . Внесите следующие 3 изменения в _MyHomePageState :

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

// ...

class _MyHomePageState extends State<MyHomePage> {

  var selectedIndex = 0;     // ← Add this property.

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Row(
        children: [
          SafeArea(
            child: NavigationRail(
              extended: false,
              destinations: [
                NavigationRailDestination(
                  icon: Icon(Icons.home),
                  label: Text('Home'),
                ),
                NavigationRailDestination(
                  icon: Icon(Icons.favorite),
                  label: Text('Favorites'),
                ),
              ],
              selectedIndex: selectedIndex,    // ← Change to this.
              onDestinationSelected: (value) {

                // ↓ Replace print with this.
                setState(() {
                  selectedIndex = value;
                });

              },
            ),
          ),
          Expanded(
            child: Container(
              color: Theme.of(context).colorScheme.primaryContainer,
              child: GeneratorPage(),
            ),
          ),
        ],
      ),
    );
  }
}

// ...

Изучите изменения:

  1. Вы вводите новую переменную selectedIndex и инициализируете ее значением 0 .
  2. Вы используете эту новую переменную в определении NavigationRail вместо жестко закодированного 0 , который был там до сих пор.
  3. Когда вызывается обратный вызов onDestinationSelected , вместо того, чтобы просто печатать новое значение на консоли, вы присваиваете его selectedIndex внутри вызова setState() . Этот вызов аналогичен методу notifyListeners() использованному ранее — он обеспечивает обновление пользовательского интерфейса.

Навигационная направляющая теперь реагирует на действия пользователя. Но расширенная область справа остается прежней. Это связано с тем, что код не использует selectedIndex для определения того, какой экран будет отображаться.

Использовать выбранныйиндекс

Поместите следующий код в начало метода build _MyHomePageState , непосредственно перед return Scaffold :

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

// ...

Widget page;
switch (selectedIndex) {
  case 0:
    page = GeneratorPage();
    break;
  case 1:
    page = Placeholder();
    break;
  default:
    throw UnimplementedError('no widget for $selectedIndex');
}

// ...

Изучите этот фрагмент кода:

  1. В коде объявляется новая переменная page типа Widget .
  2. Затем оператор переключения назначает экран page в соответствии с текущим значением в selectedIndex .
  3. Поскольку FavoritesPage пока нет, используйте Placeholder ; удобный виджет, который рисует перечеркнутый прямоугольник, где бы вы его ни разместили, отмечая эту часть пользовательского интерфейса как незавершенную.

5685cf886047f6ec.png

  1. Применяя принцип отказоустойчивости , оператор switch также гарантирует выдачу ошибки, если selectedIndex не равен ни 0, ни 1. Это помогает предотвратить ошибки в дальнейшем. Если вы когда-нибудь добавите новый пункт назначения в навигационную панель и забудете обновить этот код, программа выйдет из строя в процессе разработки (в отличие от того, чтобы позволить вам догадаться, почему что-то не работает, или позволить вам опубликовать ошибочный код в производство).

Теперь, когда page содержит виджет, который вы хотите отобразить справа, вы, вероятно, можете догадаться, какие еще изменения необходимы.

Вот _MyHomePageState после этого единственного оставшегося изменения:

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

// ...

class _MyHomePageState extends State<MyHomePage> {
  var selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    Widget page;
    switch (selectedIndex) {
      case 0:
        page = GeneratorPage();
        break;
      case 1:
        page = Placeholder();
        break;
      default:
        throw UnimplementedError('no widget for $selectedIndex');
    }

    return Scaffold(
      body: Row(
        children: [
          SafeArea(
            child: NavigationRail(
              extended: false,
              destinations: [
                NavigationRailDestination(
                  icon: Icon(Icons.home),
                  label: Text('Home'),
                ),
                NavigationRailDestination(
                  icon: Icon(Icons.favorite),
                  label: Text('Favorites'),
                ),
              ],
              selectedIndex: selectedIndex,
              onDestinationSelected: (value) {
                setState(() {
                  selectedIndex = value;
                });
              },
            ),
          ),
          Expanded(
            child: Container(
              color: Theme.of(context).colorScheme.primaryContainer,
              child: page,  // ← Here.
            ),
          ),
        ],
      ),
    );
  }
}


// ...

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

Отзывчивость

Затем сделайте навигационную рейку отзывчивой. То есть сделать так, чтобы метки автоматически отображались (используя extended: true ), когда для них достаточно места.

a8873894c32e0d0b.png

Flutter предоставляет несколько виджетов, которые помогут вам автоматически реагировать на ваши приложения. Например, Wrap — это виджет, похожий на Row или Column , который автоматически переносит дочерние элементы на следующую «строку» (называемую «run»), когда недостаточно вертикального или горизонтального пространства. Существует FittedBox , виджет, который автоматически помещает своего дочернего элемента в доступное пространство в соответствии с вашими требованиями.

Но NavigationRail не отображает метки автоматически , когда достаточно места, поскольку не может знать, сколько места достаточно в каждом контексте. Это решение зависит от вас, разработчика.

Допустим, вы решили показывать метки только в том случае, если ширина MyHomePage составляет не менее 600 пикселей.

В данном случае используется виджет LayoutBuilder . Он позволяет вам изменять дерево виджетов в зависимости от того, сколько у вас свободного места.

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

  1. Внутри метода build _MyHomePageState поместите курсор на Scaffold .
  2. Вызовите меню «Рефакторинг» с помощью Ctrl+. (Windows/Linux) или Cmd+. (Мак).
  3. Выберите «Обтекание с помощью Builder» и нажмите Enter .
  4. Измените имя только что добавленного Builder на LayoutBuilder .
  5. Измените список параметров обратного вызова с (context) на (context, constraints) .

Обратный вызов builder LayoutBuilder вызывается каждый раз, когда изменяются ограничения. Это происходит, когда, например:

  • Пользователь изменяет размер окна приложения
  • Пользователь поворачивает свой телефон из портретного режима в альбомный или обратно.
  • Некоторые виджеты рядом с MyHomePage увеличиваются в размерах, уменьшая ограничения MyHomePage .
  • И так далее

Теперь ваш код может решить, показывать ли метку, запрашивая текущие constraints . Внесите следующее однострочное изменение в метод build _MyHomePageState :

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

// ...

class _MyHomePageState extends State<MyHomePage> {
  var selectedIndex = 0;

  @override
  Widget build(BuildContext context) {
    Widget page;
    switch (selectedIndex) {
      case 0:
        page = GeneratorPage();
        break;
      case 1:
        page = Placeholder();
        break;
      default:
        throw UnimplementedError('no widget for $selectedIndex');
    }

    return LayoutBuilder(builder: (context, constraints) {
      return Scaffold(
        body: Row(
          children: [
            SafeArea(
              child: NavigationRail(
                extended: constraints.maxWidth >= 600,  // ← Here.
                destinations: [
                  NavigationRailDestination(
                    icon: Icon(Icons.home),
                    label: Text('Home'),
                  ),
                  NavigationRailDestination(
                    icon: Icon(Icons.favorite),
                    label: Text('Favorites'),
                  ),
                ],
                selectedIndex: selectedIndex,
                onDestinationSelected: (value) {
                  setState(() {
                    selectedIndex = value;
                  });
                },
              ),
            ),
            Expanded(
              child: Container(
                color: Theme.of(context).colorScheme.primaryContainer,
                child: page,
              ),
            ),
          ],
        ),
      );
    });
  }
}


// ...

Теперь ваше приложение реагирует на окружающую среду, например размер экрана, ориентацию и платформу! Другими словами, он отзывчив!

Остается только заменить этот Placeholder реальным экраном избранного . Это описано в следующем разделе.

8. Добавьте новую страницу

Помните виджет Placeholder , который мы использовали вместо страницы «Избранное» ?

Пришло время это исправить.

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

Вот несколько указателей:

  • Если вам нужен прокручиваемый Column , используйте виджет ListView .
  • Помните, что доступ к экземпляру MyAppState из любого виджета осуществляется с помощью context.watch<MyAppState>() .
  • Если вы также хотите попробовать новый виджет, у ListTile есть такие свойства, как title (обычно для текста), leading (для значков или аватаров) и onTap (для взаимодействия). Однако вы можете добиться аналогичного эффекта с помощью уже знакомых вам виджетов.
  • Dart позволяет использовать циклы for внутри литералов коллекции. Например, если messages содержат список строк, вы можете использовать следующий код:

f0444bba08f205aa.png

С другой стороны, если вы более знакомы с функциональным программированием, Dart также позволяет вам писать код типа messages.map((m) => Text(m)).toList() . И, конечно же, вы всегда можете создать список виджетов и обязательно дополнять его внутри метода build .

Преимущество самостоятельного добавления страницы «Избранное» заключается в том, что вы узнаете больше, принимая собственные решения. Недостаток в том, что вы можете столкнуться с проблемами, которые пока не сможете решить самостоятельно. Помните: неудачи – это нормально, и это один из наиболее важных элементов обучения. Никто не ожидает, что вы справитесь с разработкой Flutter в первый же час, и вам не следует этого делать.

252f7c4a212c94d2.png

Далее следует лишь один из способов реализации страницы избранного. То, как это реализовано (надеюсь), вдохновит вас поиграть с кодом — улучшить пользовательский интерфейс и сделать его своим.

Вот новый класс FavoritesPage :

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

// ...

class FavoritesPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var appState = context.watch<MyAppState>();

    if (appState.favorites.isEmpty) {
      return Center(
        child: Text('No favorites yet.'),
      );
    }

    return ListView(
      children: [
        Padding(
          padding: const EdgeInsets.all(20),
          child: Text('You have '
              '${appState.favorites.length} favorites:'),
        ),
        for (var pair in appState.favorites)
          ListTile(
            leading: Icon(Icons.favorite),
            title: Text(pair.asLowerCase),
          ),
      ],
    );
  }
}

Вот что делает виджет:

  • Он получает текущее состояние приложения.
  • Если список избранного пуст, по центру отображается сообщение: Избранного пока нет *.*
  • В противном случае отображается (прокручиваемый) список.
  • Список начинается с краткой информации (например, У вас 5 избранных *.*).
  • Затем код перебирает все избранные и создает виджет ListTile для каждого из них.

Теперь остается только заменить виджет Placeholder на FavoritesPage . И вуаля!

Вы можете получить окончательный код этого приложения в репозитории codelab на GitHub.

9. Следующие шаги

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

Посмотри на себя! Вы взяли нефункциональный каркас со Column и двумя виджетами Text и превратили его в отзывчивое, восхитительное маленькое приложение.

d6e3d5f736411f13.png

Что мы рассмотрели

  • Основы работы Flutter
  • Создание макетов во Flutter
  • Связывание действий пользователя (например, нажатий кнопок) с поведением приложения.
  • Поддержание порядка в коде Flutter
  • Делаем ваше приложение адаптивным
  • Достижение единообразного внешнего вида вашего приложения.

Что дальше?

  • Больше экспериментируйте с приложением, которое вы написали во время этой лабораторной работы.
  • Посмотрите код этой расширенной версии того же приложения, чтобы узнать, как можно добавлять анимированные списки, градиенты, плавные переходы и многое другое.
  • Следите за своим учебным процессом, перейдя на flutter.dev/learn .