Ваше первое приложение 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
  • Android
  • Windows
  • macOS
  • Linux
  • веб

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

16695777c07f18e5.png

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

Выбор веб-среды в качестве целевой платформы разработки может показаться заманчивым. Недостаток такого выбора заключается в потере одной из самых полезных функций Flutter: горячей перезагрузки с сохранением состояния (Stateful Hot Reload). Flutter не поддерживает горячую перезагрузку веб-приложений.

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

Установите Flutter

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

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

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

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

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

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

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

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

Запустите Visual Studio Code и откройте палитру команд (нажав F1 , Ctrl+Shift+P или Shift+Cmd+P ). Начните вводить "flutter new". Выберите команду "Flutter: Новый проект ".

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

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

260a7d97f9678005.png

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

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

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

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

e2a5bab0be07f4f7.png

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

pubspec.yaml

name: namer_app
description: "A new Flutter project."
publish_to: "none"
version: 0.1.0

environment:
  sdk: ^3.9.0

dependencies:
  flutter:
    sdk: flutter
  english_words: ^4.0.0
  provider: ^6.1.5

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^6.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

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

lib/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(
          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 ). Например:

lib/main.dart

// ...

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

// ...

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

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

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

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

lib/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 , чтобы понять, как он работает.

lib/main.dart

// ...

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

// ...

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

lib/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(
          colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepOrange),
        ),
        home: MyHomePage(),
      ),
    );
  }
}

// ...

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

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

lib/main.dart

// ...

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

// ...

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

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

d9b6ecac5494a6ff.png

lib/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 .

lib/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 из функции обратного вызова кнопки.

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

lib/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+. (Windows/Linux) или Cmd+. (Mac).

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

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

lib/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 , откройте меню «Refactor» и выберите «Wrap with widget...» .

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

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

lib/main.dart

// ...

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

  final WordPair pair;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Text(pair.asLowerCase),
      ),
    );
  }
}

// ...

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

6031adbc0a11e16b.png

Тема и стиль

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

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

lib/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 .

lib/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 — это крупный стиль, предназначенный для отображаемого текста. Слово display здесь используется в типографическом смысле, например, в шрифте display . В документации к displayMedium говорится, что «стили display зарезервированы для короткого, важного текста» — именно то, что нам нужно.
  • Теоретически свойство displayMedium темы может быть равно null . Dart, язык программирования, на котором вы пишете это приложение, является null-безопасным, поэтому он не позволит вам вызывать методы объектов, которые потенциально могут быть null . Однако в этом случае вы можете использовать оператор ! (оператор восклицательного знака), чтобы заверить Dart, что вы знаете, что делаете. (В этом случае displayMedium точно не равен null. Причина, по которой мы это знаем, выходит за рамки этого практического занятия.)
  • Вызов метода copyWith() для displayMedium возвращает копию стиля текста с заданными вами изменениями. В данном случае вы изменяете только цвет текста.
  • Чтобы получить новый цвет, вам снова нужно получить доступ к теме приложения. Свойство onPrimary цветовой схемы определяет цвет, который хорошо подходит для использования в качестве основного цвета приложения.

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

2405e9342d28c193.png

Если захотите, можете дополнительно изменить карту. Вот несколько идей:

  • copyWith() позволяет изменять гораздо больше параметров стиля текста, чем просто цвет. Чтобы получить полный список изменяемых свойств, поместите курсор в любое место внутри скобок функции copyWith() и нажмите Ctrl+Shift+Space (Windows/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 , чтобы переопределить визуальное содержимое текстового виджета семантическим содержимым, более подходящим для программ чтения с экрана:

lib/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 и внесите следующее изменение:

lib/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 . Это можно проверить с помощью инспектора виджетов .

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

Вы можете просто центрировать саму колонку. Наведите курсор на Column , вызовите меню «Рефакторинг» (с помощью Ctrl+. или Cmd+. ) и выберите «Перенос по центру» .

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

455688d93c30d154.png

При желании вы можете еще немного подкорректировать это.

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

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

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

lib/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 не занимать все доступное горизонтальное пространство.

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

lib/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 , чтобы кнопки были немного разнесены друг от друга.

lib/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 на 2 отдельных виджета.

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

lib/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 . SelectedIndex, равный нулю, выбирает первый пункт назначения, selectedIndex, равный единице, выбирает второй пункт назначения и так далее. На данный момент он жестко задан равным нулю.
  • Панель навигации также определяет, что происходит, когда пользователь выбирает один из пунктов назначения с помощью onDestinationSelected . В данный момент приложение просто выводит запрошенное значение индекса с помощью print() .
  • Вторым дочерним элементом элемента Row является виджет Expanded . Виджеты 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 :

lib/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 :

lib/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. Затем оператор switch назначает экран page в соответствии с текущим значением в selectedIndex .
  3. Поскольку FavoritesPage пока нет, используйте Placeholder ; это удобный инструмент, который рисует перечеркнутый прямоугольник в том месте, где вы его разместите, отмечая эту часть пользовательского интерфейса как незавершенную.

5685cf886047f6ec.png

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

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

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

lib/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 provides several widgets that help you make your apps automatically responsive. For example, Wrap is a widget similar to Row or Column that automatically wraps children to the next "line" (called "run") when there isn't enough vertical or horizontal space. There's FittedBox , a widget that automatically fits its child into available space according to your specifications.

But NavigationRail doesn't automatically show labels when there's enough space because it can't know what is enough space in every context. It's up to you, the developer, to make that call.

Say you decide to show labels only if MyHomePage is at least 600 pixels wide.

The widget to use, in this case, is LayoutBuilder . It lets you change your widget tree depending on how much available space you have.

Once again, use Flutter's Refactor menu in VS Code to make the required changes. This time, though, it's a little more complicated:

  1. Inside _MyHomePageState 's build method, put your cursor on Scaffold .
  2. Call up the Refactor menu with Ctrl+. (Windows/Linux) or Cmd+. (Mac).
  3. Select Wrap with Builder and press Enter .
  4. Modify the name of the newly added Builder to LayoutBuilder .
  5. Modify the callback parameter list from (context) to (context, constraints) .

LayoutBuilder 's builder callback is called every time the constraints change. This happens when, for example:

  • The user resizes the app's window
  • The user rotates their phone from portrait mode to landscape mode, or back
  • Some widget next to MyHomePage grows in size, making MyHomePage 's constraints smaller

Now your code can decide whether to show the label by querying the current constraints . Make the following single-line change to _MyHomePageState 's build method:

lib/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,
              ),
            ),
          ],
        ),
      );
    });
  }
}


// ...

Now, your app responds to its environment, such as screen size, orientation, and platform! In other words, it's responsive!.

The only work that remains is to replace that Placeholder with an actual Favorites screen. That's covered in the next section.

8. Add a new page

Remember the Placeholder widget we used instead of the Favorites page?

Пора это исправить.

If you feel adventurous, try to do this step by yourself. Your goal is to show the list of favorites in a new stateless widget, FavoritesPage , and then show that widget instead of the Placeholder .

Here are a few pointers:

  • When you want a Column that scrolls, use the ListView widget.
  • Remember, access the MyAppState instance from any widget using context.watch<MyAppState>() .
  • If you also want to try a new widget, ListTile has properties like title (generally for text), leading (for icons or avatars) and onTap (for interactions). However, you can achieve similar effects with the widgets you already know.
  • Dart allows using for loops inside collection literals. For example, if messages contains a list of strings, you can have code like the following:

f0444bba08f205aa.png

On the other hand, if you're more familiar with functional programming, Dart also lets you write code like messages.map((m) => Text(m)).toList() . And, of course, you can always create a list of widgets and imperatively add to it inside the build method.

The advantage of adding the Favorites page yourself is that you learn more by making your own decisions. The disadvantage is that you might run into trouble that you aren't yet able to solve by yourself. Remember: failing is okay, and is one of the most important elements of learning. Nobody expects you to nail Flutter development in your first hour, and neither should you.

252f7c4a212c94d2.png

What follows is just one way to implement the favorites page. How it's implemented will (hopefully) inspire you to play with the code—improve the UI and make it your own.

Here's the new FavoritesPage class:

lib/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),
          ),
      ],
    );
  }
}

Here's what the widget does:

  • It gets the current state of the app.
  • If the list of favorites is empty, it shows a centered message: No favorites yet.
  • Otherwise, it shows a (scrollable) list.
  • The list starts with a summary (for example, You have 5 favorites. ).
  • The code then iterates through all the favorites, and constructs a ListTile widget for each one.

All that remains now is to replace the Placeholder widget with a FavoritesPage . And voilá!

You can get the final code of this app in the codelab repo on GitHub.

9. Next steps

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

Look at you! You took a non-functional scaffold with a Column and two Text widgets, and made it into a responsive, delightful little app.

d6e3d5f736411f13.png

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

  • The basics of how Flutter works
  • Creating layouts in Flutter
  • Connecting user interactions (like button presses) to app behavior
  • Keeping your Flutter code organized
  • Making your app responsive
  • Achieving a consistent look & feel of your app

Что дальше?

  • Experiment more with the app you wrote during this lab.
  • Look at the code of this advanced version of the same app, to see how you can add animated lists, gradients, cross-fades, and more.