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

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

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

subjectПоследнее обновление: мая 19, 2025
account_circleАвторы: Filip Hracek

1. Введение

Flutter — это набор инструментов Google для создания UI-приложений для мобильных устройств, веб-сайтов и настольных компьютеров из единой кодовой базы. В этой кодовой лаборатории вы создадите следующее приложение 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: Stateful Hot Reload. Flutter не может выполнять горячую перезагрузку веб-приложений.

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

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

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

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

  1. Пакет SDK для Flutter
  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: New Project .

Далее выберите Application , а затем папку, в которой вы хотите создать свой проект. Это может быть ваш домашний каталог или что-то вроде 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' # Remove this line if you wish to publish to pub.dev

version: 0.0.1+1

environment:
  sdk: ^3.8.0

dependencies:
  flutter:
    sdk: flutter

  english_words: ^4.0.0
  provider: ^6.1.5

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^5.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: [
         
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 Hot Reload с сохранением состояния в действии. Горячая перезагрузка запускается, когда вы сохраняете изменения в исходном файле.

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

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

Затем добавьте кнопку внизу 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 минут

Как бы весело ни было наблюдать за Debug Console , вы хотите, чтобы кнопка делала что-то более значимое. Однако прежде чем приступить к этому, внимательно изучите код в 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 настраивает все приложение. Он создает состояние всего приложения (подробнее об этом позже), называет приложение, определяет визуальную тему и устанавливает виджет «home» — начальную точку вашего приложения.

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 .

Теперь вызовите меню Refactor . В VS Code это можно сделать двумя способами:

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

ИЛИ

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

В меню Refactor выберите Extract Widget . Назначьте имя, например 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() внутри него. Как и прежде, вызовите меню Refactor на виджете Text . Однако на этот раз вы не собираетесь извлекать виджет.

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

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

Далее, поднимитесь на уровень выше. Наведите курсор на виджет Padding , вызовите меню Refactor и выберите Wrap with widget... .

Это позволяет указать родительский виджет. Введите «Карта» и нажмите 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 typeface . В документации displayMedium говорится, что «стили отображения зарезервированы для короткого, важного текста» — как раз наш вариант использования.
  • Свойство темы displayMedium теоретически может быть null . Dart, язык программирования, на котором вы пишете это приложение, является null-безопасным, поэтому он не позволит вам вызывать методы объектов, которые потенциально являются null . В этом случае, однако, вы можете использовать оператор ! («оператор bang»), чтобы гарантировать Dart, что вы знаете, что делаете. ( displayMedium определенно не равен null в этом случае. Однако причина, по которой мы это знаем, выходит за рамки этой кодовой лаборатории.)
  • Вызов 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 , чтобы переопределить визуальное содержимое текстового виджета семантическим содержимым, которое больше подходит для программ чтения с экрана:

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 . Мы можем проверить это с помощью Widget Inspector .

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

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

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

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

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

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

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

При сохранении вы заметите, что 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 на два отдельных виджета.

Выберите все 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 . Выбранный индекс ноль выбирает первый пункт назначения, выбранный индекс один выбирает второй пункт назначения и т. д. На данный момент он жестко закодирован на ноль.
  • Навигационная панель также определяет, что происходит, когда пользователь выбирает один из пунктов назначения с помощью 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... ) и вызовите меню Refactor с помощью Ctrl+. или Cmd+. . Затем выберите Convert to StatefulWidget .

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

установитьСостояние

Новый виджет с отслеживанием состояния должен отслеживать только одну переменную: 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 для определения того, какой экран отображать.

Использовать 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. Затем оператор переключения назначает экран на 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 предоставляет несколько виджетов, которые помогают вам автоматически реагировать ваши приложения. Например, Wrap - это виджет, аналогичный Row или Column , который автоматически завершает детей на следующую «линию» (называемой «run»), когда не хватает вертикального или горизонтального пространства. Там есть FittedBox , виджет, который автоматически подходит для своего ребенка в доступное пространство в соответствии с вашими спецификациями.

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

Скажем, вы решили показать этикетки, только если MyHomePage имеет ширину не менее 600 пикселей.

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

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

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

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

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

Теперь ваш код может решить, показывать ли этикетку, запрашивая текущие constraints . Сделайте следующее изменение однолинейного изменения в метод build _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 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 содержит список строк, вы можете иметь код, например, следующее:

F04444BBA08F205AA.PNG

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

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

252F7C4A212C94D2.png

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

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

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

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

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

Все, что осталось сейчас, - это заменить виджет Placeholder на FavoritesPage . И вуаля!

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

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

Поздравляю!

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

D6E3D5F736411F13.png

То, что мы покрыли

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

Что дальше?

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