Прототипирование мобильных приложений для разных платформ с использованием Flutter и AWS Amplify



I’m going to show you how you can use Flutter and AWS Amplify to quickly go from nothing to a working cross-platform mobile application with authentication and backend infrastructure. What would usually take a small dev team a week or so to setup can be achieved in a fraction of the time using this toolkit.

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

Вот готовое изделие. Если вы хотите “здесь вариант, который я сделал заранее”, следуйте инструкциям в readme, и у вас должно получиться запустить его примерно за пятнадцать минут. Вот ссылка на GitHub

Этот учебник состоит из пяти частей:

  1. Необходимые условия и настройка кодовой базы
  2. Добавление аутентификации
  3. Загрузка фото профиля
  4. Хранение данных пользователя
  5. Добавление элементов дизайна

Рекомендация

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

Amplify также является мощной платформой; однако мне было трудно работать с функциональностью API, и библиотеки Flutter не были обновлены до последних объявлений и функций в Amplify. В частности, работа с AppSync GraphQL и DataStore (для офлайн-хранения данных и синхронизации) была довольно хрупкой (как вы увидите позже).

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

Приложение-демо, которое я создал, хранит информацию о профиле пользователя — распространенное требование для многих приложений. Вы можете создать учетную запись и войти, загрузить аватар и представить некоторые данные о себе. Мы подробно рассмотрим весь стек — работа с Flutter и Dart для кода приложения, а также такие инструменты, как DynamoDB, чтобы предоставить вам полный обзор того, что вам нужно знать.

Часть первая: Предварительные требования и настройка кодовой базы

Данное руководство предполагает, что у вас уже настроены следующие компоненты на вашем компьютере:

Code Editor/ IDE I use VSCode as it has a good set of Flutter and Dart plugins that speed up development, such as auto loading of dependencies, Dart linting, and intellisense. You’re free to use whatever IDE works for you though
AWS Account Create an AWS account if you don’t already have one. Visit AWS’ official page for steps to create an AWS account.

All of what we’ll use today is part of the free tier, so it shouldn’t cost you anything to follow this tutorial.

AWS CLI and AWS Amplify CLI Install AWS and Amplify CLI tooling.

Make sure you have an up-to-date version of Node and NPM (this is what the CLIs use). Visit Node.Js’ official website to download the up-to-date version.

If you need to run multiple versions of Node, I recommend using NVM to manage and switch between them.

To install AWS CLI, visit AWS’ official page.

XCode (for iOS) If you don’t have access to a Mac, you can deploy EC2 instances running MacOS in AWS these days, which you can use when you need to build iOS artifacts.

Download Xcode through the Mac App Store.

Follow the rest of the steps here to set it up ready for iOS Flutter development.

Android Studio (for Android) Follow the steps here to be ready for Android Flutter development.
Flutter SDK Follow these steps to get Flutter and its dependencies up and running (if you’re on a Mac that is, other guides are available for other OSes).

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

Убедитесь, что вы создали структуру кодовой базы с использованием Flutter, а затем инициализировали Amplify в ней.

I used the official Flutter getting started documentation to kick things off for my demo.

Давайте проверим, работает ли Flutter. Во-первых, чтобы убедиться, что он установлен правильно и добавлен в ваш PATH, вы можете запустить flutter doctor.

Если это ваш первый опыт в мобильной разработке, здесь потребуется устранение нескольких моментов. Для меня это было:

  • Установка Android Studio (и Android SDK CLI).
  • Установка XCode и CocoaPods.
  • Согласие с условиями и соглашениями для инструментов Android и iOS.

Создание кодовой базы вашего приложения

Когда все предварительные требования будут готовы, вы можете создать каркас Flutter. Это создает папку, в которой мы будем работать, поэтому запустите эту команду из родительской директории:

 
flutter create flutterapp --platforms=android,ios

I’ve specified Android and iOS as target platforms to remove the unnecessary config for other platforms (e.g. web, Windows, Linux).


Возможно, вы захотите переименовать корневую директорию, созданную на данном этапе, если вы не хотите, чтобы она совпадала с названием вашего приложения. Я изменил её с “flutterapp” на “flutter-amplify-tutorial” (название моего репозитория git).

На данный момент Flutter создал для нас семьдесят три файла. Давайте посмотрим, что это такое:


Папки, с которыми мы будем проводить большую часть времени, это ios/android и lib/. Внутри папок ios и android находятся проектные ресурсы, которые можно открыть с помощью XCode и Android Studio соответственно. Эти проекты выступают в качестве интерфейса между платформо-независимой Dart-код и вашими целевыми платформами, и вы можете использовать их для тестирования вашего приложения на соответствующих платформах. Давайте попробуем это с iOS сейчас:

Настройка iOS

 

open -a Simulator
flutter run

На моем Mac, с минимальной настройкой XCode, это прошло от нуля до запуска симулятора iPhone 14 Pro Max с заготовленным Flutter приложением, что довольно круто. 


Если вы видите следующее: то поздравляю, вы успешно сгенерировали каркас.


Вы также можете открыть проект ios/Runner.xcodeproj внутри XCode, изучить его содержимое и запустить на симуляторах и физических устройствах, как вы делаете это с любым другим проектом XCode.

Настройка Android

Android немного менее прямолинеен, так как вам нужно явно настроить эмулятор в Android Studio, прежде чем вы сможете запустить его. Откройте проект android/flutterapp_android.iml внутри Android Studio, чтобы начать, и затем вы можете настроить и запустить эмулятор для тестирования приложения.


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

Когда Android Studio уляжется, если у вас уже настроен имитатор устройства в AVD, вы должны иметь возможность нажать кнопку “воспроизвести” в верхнем правом углу окна:


И вот, чудо, то же приложение на Android:


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

Это хорошее время для коммита в git, теперь у нас есть основа для разработки на Flutter. Теперь мы находимся в точке, где можем начать экспериментировать с кодом Flutter и видеть наши результаты одновременно на iOS и Android.

Flutter использует Dart в качестве промежуточного языка между Android и iOS, и весь код, с которым вы будете взаимодействовать, находится в папке lib/. Там должен быть файл main.dart, с которого мы начнем экспериментировать.

Настройка и развертывание нового приложения с помощью Amplify

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

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

Для начала, в вашей папке с кодом запустите следующее:

 
amplify init

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



Это обеспечит некоторые начальные ресурсы AWS для хранения конфигурации и состояния вашего приложения Amplify, а именно бакет S3.

Прогресс и статус вышеупомянутого развертывания могут показаться знакомыми некоторым — это CloudFormation, и так же, как AWS CDK, Amplify использует CFN за кулисами для предоставления всех необходимых ресурсов. Вы можете открыть консоль стеков CloudFormation, чтобы увидеть это в действии:


Наконец, когда CLI завершит работу, вы должны увидеть подтверждение, подобное приведенному ниже, и вы сможете увидеть свое недавно развернутое приложение в консоли Amplify:



Управление окружениями

AWS Amplify имеет понятие «окружений», которые представляют собой изолированные развертывания вашего приложения и ресурсов. Исторически, понятие окружений должно было создаваться в любой экосистеме, которую вы использовали: (например, CloudFormation, CDK), используя такие вещи, как соглашения о наименованиях и параметры. В Amplify это первоклассное понятие — вы можете иметь несколько окружений, которые позволяют такие шаблоны, как предоставление общих окружений, через которые продвигаются изменения (например, Dev > QA > PreProd > Prod), а также предоставление окружений для каждого разработчика или фичи-ветки.

Amplify также может настроить и подготовить службы CI/CD для вас, используя Amplify hosting add, и интегрировать их в ваши приложения для предоставления всеобъемлющей экосистемы разработки. Это настраивает CodeCommit, CodeBuild и CodeDeploy для управления исходным контролем, сборки и развертывания приложений. Это не рассматривается в этом учебнике, но может использоваться для автоматизации сборки, тестирования и публикации релизов в магазинах приложений.

Часть вторая: Добавление аутентификации

Обычно вам нужно было бы изучить службу аутентификации AWS Cognito и поддерживающие службы, такие как IAM, соединить все это с помощью чего-то вроде CloudFormation, Terraform или CDK. В Amplify это так же просто, как:

 
amplify add auth

Amplify add позволяет добавлять в ваш проект различные “функции”. За кулисами Amplify развернет и настроит все необходимые услуги с использованием CloudFormation, так что вы можете сосредоточиться больше на функциях ваших приложений и меньше на сантехнике.

Когда я говорю, что это так же просто, как ввод этих трех магических слов выше… это не совсем так. Amplify задаст вам различные вопросы, чтобы понять, как вы хотите аутентифицировать людей и какие контроли вы хотите установить. Если вы выберете “Default Configuration“, Amplify настроит аутентификацию с разумными настройками, чтобы быстро начать работу. Я собираюсь выбрать “Manual Configuration” для демонстрации того, насколько настраиваемым является Amplify.


Вышеописанная настройка позволяет создавать учетные записи только с использованием вашего мобильного номера (без необходимости указывать адрес электронной почты) и проверять, что вы являетесь фактическим владельцем этого номера, используя MFA для верификации и последующих попыток входа. Я рекомендую использовать OAuth как стандартизированный механизм аутентификации, но здесь я не использовал его для простоты.

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

Для развертывания функций (или любых изменений конфигурации) вам нужно выполнить push:

 
amplify push

Примечание: это отличается от команды amplify publish, которая создает и развертывает как сервисы бэкенда, так и фронтенда. Push только настраивает ресурсы бэкенда (и это все, что нам понадобится в этом учебнике, так как мы будем создавать мобильные приложения).

Когда вы добавляете аутентификацию (или любую функцию Amplify), Amplify добавляет файл на языке Dart под названием lib/amplifyconfiguration.dart. Этот файл игнорируется git, так как он содержит чувствительные учетные данные, связанные с развернутыми ресурсами, и автоматически синхронизируется с окружением Amplify, в котором вы работаете. Более подробную информацию об этом можно найти здесь.

На данном этапе мы настроили Amplify с приложением и средой разработки, а также настроили Cognito для аутентификации. Если вы следовать этому руководству, то хорошее время для git commit, чтобы иметь возможность вернуться к этой точке при необходимости. Amplify должен был уже создать файл .gitignore для вас, исключая все ненужные файлы.

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

Аутентификация пользователей в нашем приложении

I’m following the steps outlined in the authentication for the AWS Amplify tutorial here.

Для этого используются стандартные экраны и рабочий процесс аутентификации, включенные в amplify_flutter. Добавьте зависимости Amplify Flutter, добавив следующее под ” dependencies ” в файле pubspec.yaml:

 

amplify_flutter: ^0.6.0
amplify_auth_cognito: ^0.6.0

Если вы не используете расширения Flutter и Dart в VSCode (или не используете VSCode), вам нужно будет выполнить команду flutter pub get после этого. Если да, то VSCode автоматически запустит это при сохранении файла pubspec.yaml.

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

Шаги по интеграции библиотеки аутентификации OOTB приведены здесь.

Мы можем транспонировать виджет аутентификатора, настроенный в примере кода, над кодом, предоставленным в примере быстрого старта Flutter, следующим образом:

 

class _MyAppState extends State {
  @override
  Widget build(BuildContext context) {
    return Authenticator(
        child: MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
          // Это тема вашего приложения.
          //
          // Попробуйте запустить ваше приложение с помощью "flutter run". Вы увидите,
          // что у приложения есть синяя панель инструментов. Затем, не закрывая приложение, попробуйте
          // изменить primarySwatch ниже на Colors.green и затем вызовите
          // "горячую перезагрузку" (нажмите "r" в консоли, где вы запустили "flutter run",
          // или просто сохраните свои изменения для "горячей перезагрузки" в IDE Flutter).
          // Обратите внимание, что счетчик не сбросился обратно к нулю; приложение
          // не перезапускается.
          primarySwatch: Colors.blue,
          useMaterial3: true),
      home: const MyHomePage(title: 'Flutter Amplify Quickstart'),
      builder: Authenticator.builder(),
    ));
  }

  @override
  void initState() {
    super.initState();
    _configureAmplify();
  }

  void _configureAmplify() async {
    try {
      await Amplify.addPlugin(AmplifyAuthCognito());
      await Amplify.configure(amplifyconfig);
    } on Exception catch (e) {
      print('Error configuring Amplify: $e');
    }
  }
}

Что такое виджет?

Это основной строительный блок в Flutter, используемый для составления макетов пользовательского интерфейса и компонентов. Практически все в макетах Flutter являются виджетами — колонки, каркас, отступы и стили, сложные компоненты и т.д. Пример в документах по началу работы с Flutter использует виджет “Center”, за которым следует виджет “Text” для отображения центрально выровненного текста, который говорит “Hello World.”

Вышеприведенный код украшает виджет MyHomePage виджетом аутентификатора, добавляет плагин AmplifyAuthCognito и использует конфигурацию, которую предыдущая команда amplify add auth сгенерировала в lib/amplifyconfiguration.dart для автоматического подключения к вашему AWS Cognito User Pool.

После запуска Flutter, запуская демонстрацию интеграции аутентификации, мне потребовалось некоторое время для завершения шага “Running pod install” (почти 5 минут). Просто будьте терпеливы.


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



Используя поток “Создать аккаунт”, вы можете ввести свой номер телефона и пароль, и затем вам предложат выполнить вызов многофакторной аутентификации для завершения регистрации. Затем вы можете увидеть, что пользователь создан в пуле пользователей Cognito:


Вы можете легко протестировать это на виртуальном устройстве Android. Вам даже не нужно выходить из VSCode, если вы установили плагины Flutter и Dart, так что открывать Android Studio не нужно. Просто выберите имя текущего активного устройства (iPhone) в нижнем правом углу VSCode, переключитесь на виртуальное Android-устройство, которое вы уже создали, затем нажмите “F5” для начала отладки. Опыт довольно похож на iOS:



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

 
uses-sdk:minSdkVersion 16 cannot be smaller than version 21 declared in library [:amplify_auth_cognito_android]

Flutter действительно полезен в этой ситуации, так как сразу после вывода трассировки стека предлагает рекомендацию:


Flutter SDK уже, похоже, переопределяет это в нашем файле `build.gradle`:

 

apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"

android {
    ...
    defaultConfig {
        // TODO: Укажите свой уникальный идентификатор приложения (https://developer.android.com/studio/build/application-id.html).
        applicationId "com.example.flutterapp"
        // Вы можете обновить следующие значения, чтобы они соответствовали потребностям вашего приложения.
        // Для получения дополнительной информации см.: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
        minSdkVersion flutter.minSdkVersion
        targetSdkVersion flutter.targetSdkVersion
        versionCode flutterVersionCode.toInteger()
        versionName flutterVersionName
    }

Хотя Flutter, как минимум, требует использования API 16 (объявлено в flutter.gradle), библиотека Amplify Auth требует не менее 21. Чтобы исправить это, просто измените minSdkVersion с “flutter.minSdkVersion” на “21”.

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

Часть Третья: Загрузка фото профиля

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

Хотите добавить функции хранения в свое приложение? Нет проблем, просто сделайте:

 
amplify add storage

и Amplify подготовит сервисы бэкенда, необходимые для использования вашим приложением облачного хранилища. Amplify легко интегрирует Flutter с S3, что позволяет пользователям вашего приложения хранить объекты. Гибкость S3 позволяет хранить всевозможные активы, и в сочетании с Cognito и Amplify вы можете легко предоставлять частные области для пользователей для хранения фотографий, видео, файлов и т.д.

Файлы могут быть сохранены с публичным, защищенным или приватным доступом:

Public Read/Write/Delete by all users
Protected Creating Identify can Write and Delete, everyone else can Read
Private Read/Write/Delete only by Creating Identity

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

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

Шаблон начала работы с Flutter уже создает некоторые виджеты с использованием виджета MaterialApp. Ранее мы украсили это с помощью виджета аутентификатора. Теперь мы расширим виджет MyHomePage дочернего элемента MaterialApp, чтобы обеспечить изображение профиля.

Вы составляете виджеты вместе в дереве, известном как “Иерархия виджетов“. Вы всегда начинаете с виджета верхнего уровня. В нашем приложении это обертка виджета аутентификатора, которая обрабатывает начальную авторизацию. Скелет – это хороший виджет для построения макетов: он часто используется как виджет верхнего уровня в материализационных приложениях; и он имеет ряд заполнителей, таких как кнопка действий плавающего действия, нижний лист (для смахивания дополнительных деталей), панель приложения и т.д.

Сначала давайте просто добавим виджет изображения, указывающий на URL-адрес сети. Мы заменим это позже на то, что мы сделаем и загрузим в S3. Я использовал следующие ресурсы для добавления изображения с закругленным заполнителем:

  • API Flutter
  • Google Flutter

В массиве детей виджета вложенной колонки добавьте следующий виджет контейнера:

 

children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
            Container(
              width: 200,
              height: 200,
              decoration: const BoxDecoration(
                shape: BoxShape.circle,
                image: DecorationImage(
                    image: NetworkImage(
                        'https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg'),
                    fit: BoxFit.fill),
              ),
            )
          ],

Теперь мы можем отобразить изображение из веб-страницы:


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

  • Google: “Pub Dev Packages Image Picker”

Все, что требуется для того, чтобы предложить пользователю выбрать изображение, это две строки кода:

 

final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(source: ImageSource.gallery);

Для iOS необходимо добавить ключ NSPhotoLibraryUsageDescription в файл конфигурации Xcode <project root>/ios/Runner/Info.plist для запроса доступа к просмотру фотографий пользователя; в противном случае, приложение может упасть.

Мы подключим это к виджету GestureDetector, который при получении тапа предложит пользователю выбрать изображение для своего аватара:

 

ImageProvider? _image;

...

children: <Widget>[
            const Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
            GestureDetector(
                onTap: _selectNewProfilePicture,
                child: Container(
                    width: 200,
                    height: 200,
                    decoration: BoxDecoration(
                    shape: BoxShape.circle,
                    image: DecorationImage(
                        image: _image ?? _placeholderProfilePicture(), fit: BoxFit.fill),
                    ),
                ),
            )
            ...
]

void _selectNewProfilePicture() async {
    final ImagePicker picker = ImagePicker();
    final XFile? image = await picker.pickImage(source: ImageSource.gallery);

    if (image != null) {

      var imageBytes = await image.readAsBytes();

      setState(() {
        _image = MemoryImage(imageBytes);
      });
    }
  }

  _placeholderProfilePicture() {
    return const AssetImage("assets/profile-placeholder.png");
  }

Вызовите setState(), обновляя поля виджетов в лямбда-выражении, переданном в Flutter, чтобы он знал, что нужно вызвать функцию build(), где обновленное состояние может быть использовано для перерисовки виджета. В нашем случае, аватар будет заполнен, поэтому мы создадим виджет контейнера, который отображает изображение. Оператор ?? с проверкой на null предоставляет заполнитель аватара по умолчанию на случай, если пользователь еще не выбрал изображение.

Вам также потребуется добавить изображение-заполнитель аватара в ваш репозиторий и указать его в файле pubspec.yml, чтобы оно было учтено при сборке. Вы можете использовать изображение из моего репозитория, добавив это в ваш файл pubspec.yml:

 

# Следующая секция специфична для пакетов Flutter.
flutter:
...

  # Чтобы добавить ресурсы в ваше приложение, добавьте раздел assets, например, так:
  assets:
    - assets/profile-placeholder.png


На данном этапе мы можем выбрать аватар из галереи фотографий устройства и отобразить его в приложении как круглую картинку. Однако эта картинка не сохраняется нигде — как только приложение закрывается, она теряется (и другие пользователи также не смогут увидеть вашу картинку).

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

 

void _selectNewProfilePicture() async {
    final ImagePicker picker = ImagePicker();
    final XFile? image = await picker.pickImage(source: ImageSource.gallery);

    if (image != null) {

      var imageBytes = await image.readAsBytes();

      final UploadFileResult result = await Amplify.Storage.uploadFile(
          local: File.fromUri(Uri.file(image.path)),
          key: profilePictureKey,
          onProgress: (progress) {
            safePrint('Fraction completed: ${progress.getFractionCompleted()}');
          },
          options:
              UploadFileOptions(accessLevel: StorageAccessLevel.protected));

      setState(() {
        _image = MemoryImage(imageBytes);
      });
    }
  }

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

Далее мы заставим приложение загружать аватар пользователя из S3 при запуске:

 

@override
  void initState() {
    super.initState();
    _retrieveProfilePicture();
  }

  void _retrieveProfilePicture() async {
    final userFiles = await Amplify.Storage.list(
        options: ListOptions(accessLevel: StorageAccessLevel.protected));
    if (userFiles.items.any((element) => element.key == profilePictureKey)) {
      final documentsDir = await getApplicationDocumentsDirectory();
      final filepath = "${documentsDir.path}/ProfilePicture.jpg";
      final file = File(filepath);
      await Amplify.Storage.downloadFile(
          key: profilePictureKey,
          local: file,
          options:
              DownloadFileOptions(accessLevel: StorageAccessLevel.protected));

      setState(() {
        _image = FileImage(file);
      });
    } else {
      setState(() {
        _image = const AssetImage("assets/profile-placeholder.png");
      });
    }
  }

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

 

children: <Widget>[
                        Padding(
                          padding: const EdgeInsets.symmetric(vertical: 20),
                          child: Text(
                          'User Profile',
                          style: Theme.of(context).textTheme.titleLarge,
                        )),
                        const ProfilePicture(),
                        TextField(...

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

 

class _ProfilePictureState extends State<ProfilePicture> {
  ImageProvider? _image;
  bool _isLoading = true;

...

void _retrieveProfilePicture() async {
    ...
      setState(() {
        _image = FileImage(file);
        _isLoading = false;
      });
    } else {
      setState(() {
        _image = const AssetImage("assets/profile-placeholder.png");
        _isLoading = false;
      });
    }
  }

void _selectNewProfilePicture() async {
    final ImagePicker picker = ImagePicker();
    final XFile? image = await picker.pickImage(source: ImageSource.gallery);

    if (image != null) {
      setState(() {
        _isLoading = true;
      });

      ....

      setState(() {
        _image = MemoryImage(imageBytes);
        _isLoading = false;
      });
    }
  }

Четвертая часть: Хранение данных пользователя (сокращенная)

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

Обычно я бы сказал: “Вам нужен API? Просто:”

 
amplify add api

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

Amplify предоставляет высокоуровневый API для взаимодействия с данными в AppSync, но в этом уроке я буду использовать GraphQL с низкоуровневыми запросами, так как это обеспечивает большую гибкость и позволяет использовать Global Secondary Index в DynamoDB для избежания сканирования таблиц. Если вы хотите понять, как я добрался сюда и каковы различные подводные камни, проверьте “Вызовы работы с и настройкой AWS Amplify и Appsync с Flutter”.


Amplify пытается по умолчанию задавать вопросы, задаваемые при создании API, но вы можете переопределить любой из них, прокрутив вверх до опции, которую хотите изменить. В этом сценарии нам нужен GraphQL-конечная точка (для использования DataStore), и авторизация API должна быть обработана Cognito User Pool, так как мы хотим применить тонкую настройку доступа, чтобы только пользователь мог обновлять или удалять свои данные (но другие пользователи могут просматривать их).

Когда мы создадим API, Amplify создаст базовый тип схемы GraphQL для ToDo. Мы обновим это и добавим некоторые правила авторизации перед отправкой изменений API.

Измените схему GraphQL “ToDo” для удовлетворения потребностей нашего профиля пользователя информации.

 

type UserProfile @model 
@auth(rules: [
  { allow: private, operations: [read], provider: iam },
  { allow: owner, operations: [create, read, update, delete] }
])
{
  userId: String! @index
  name: String!
  location: String
  language: String
}

Приватное правило позволяет залогиненным пользователям просматривать профили других пользователей. Не используя “публичное”, мы предотвращаем доступ к профилям для лиц, не залогиненных в системе. Поставщик IAM предотвращает прямой доступ пользователей к GraphQL API — они должны использовать приложение и использовать роль “неаутентифицированный” в нашем пуле идентичности Cognito (т.е. быть вышедшими из системы) для просмотра деталей пользователя.

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

На данном этапе мы можем настроить нашу облачную инфраструктуру, поддерживающую функцию API:

 
amplify push

При изменении существующей модели GraphQL с ToDo на UserProfile, если вы ранее выполнили amplify push и настроили инфраструктуру, вы можете столкнуться с ошибкой, указывающей на то, что требуемое изменение потребует уничтожения существующей таблицы DynamoDB. Amplify предотвращает вас от этого в случае потери данных при удалении существующей таблицы ToDo. Если вы столкнетесь с этой ошибкой, вам нужно будет запустить amplify push --allow-destructive-graphql-schema-updates.

При выполнении amplify push, Amplify и CloudFormation создадут API GraphQL AppSync, промежуточные резолверы и поддерживающую таблицу DynamoDB, аналогичную следующей:



Как только мы определили схему GraphQL, мы можем использовать Amplify для генерации Dart кода, который представляет модель и слой репозитория, способного работать с API:

 
amplify codegen models

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

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

 

class _MyHomePageState extends State<MyHomePage> {
  final _nameController = TextEditingController();
  final _locationController = TextEditingController();
  final _languageController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    ...
                      children: <Widget>[
                        Padding(
                          padding: const EdgeInsets.symmetric(vertical: 20),
                          child: Text(
                          'User Profile',
                          style: Theme.of(context).textTheme.titleLarge,
                        )),
                        const ProfilePicture(),
                        TextField(
                          decoration: const InputDecoration(labelText: "Name"),
                          controller: _nameController,
                        ),
                        TextField(
                          decoration:
                              const InputDecoration(labelText: "Location"),
                          controller: _locationController,
                        ),
                        TextField(
                          decoration: const InputDecoration(
                              labelText: "Favourite Language"),
                          controller: _languageController,
                        )
                      ]

Затем соединяем наши TextFields с AppSync GraphQL API, так что когда пользователь нажимает на плавающую кнопку действий “Save“, изменения синхронизируются с DynamoDB:

 

floatingActionButton: FloatingActionButton(
            onPressed: _updateUserDetails,
            tooltip: 'Save Details',
            child: const Icon(Icons.save),
          ),
        )
      ],
    );
  }

  Future<void> _updateUserDetails() async {
    final currentUser = await Amplify.Auth.getCurrentUser();

    final updatedUserProfile = _userProfile?.copyWith(
            name: _nameController.text,
            location: _locationController.text,
            language: _languageController.text) ??
        UserProfile(
            name: _nameController.text,
            location: _locationController.text,
            language: _languageController.text);

    final request = _userProfile == null
        ? ModelMutations.create(updatedUserProfile)
        : ModelMutations.update(updatedUserProfile);
    final response = await Amplify.API.mutate(request: request).response;

    final createdProfile = response.data;
    if (createdProfile == null) {
      safePrint('errors: ${response.errors}');
    }
  }

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

 

@override
  void initState() {
    super.initState();
    _getUserProfile();
  }

void _getUserProfile() async {
    final currentUser = await Amplify.Auth.getCurrentUser();
    GraphQLRequest<PaginatedResult<UserProfile>> request = GraphQLRequest(
        document:
            '''query MyQuery { userProfilesByUserId(userId: "${currentUser.userId}") {
    items {
      name
      location
      language
      id
      owner
      createdAt
      updatedAt
      userId
    }
  }}''',
        modelType: const PaginatedModelType(UserProfile.classType),
        decodePath: "userProfilesByUserId");
    final response = await Amplify.API.query(request: request).response;

    if (response.data!.items.isNotEmpty) {
      _userProfile = response.data?.items[0];

      setState(() {
        _nameController.text = _userProfile?.name ?? "";
        _locationController.text = _userProfile?.location ?? "";
        _languageController.text = _userProfile?.language ?? "";
      });
    }
  }

Теперь у нас есть API, в который мы можем сохранять данные, защищенный с помощью Cognito и поддерживаемый DynamoDB. Довольно здорово, учитывая, что мне не пришлось писать код инфраструктуры.

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

Часть Пять: Добавление некоторых дизайнерских штрихов

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

Я не эксперт по UI, поэтому я черпал вдохновение на dribbble.com и решил нарисовать яркий фон и контрастную белую карточку для деталей профиля.

Добавление фонового изображения

Сначала я хотел добавить фоновое изображение, чтобы привнести немного цвета в приложение.

I had a go at wrapping the children of my Scaffold widget in a Container widget, which you can then apply a decoration property to. It works and it’s the more upvoted solution, but it doesn’t fill the app bar too, which would be nice.

I ended up using this approach, which utilizes a Stack widget to lay a full-height background image under our Scaffold: “Background Image for Scaffold” on Stack Overflow. 

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

 

@override
  Widget build(BuildContext context) {

    return Stack(
      children: [
        Image.asset(
          "assets/background.jpg",
          height: MediaQuery.of(context).size.height,
          width: MediaQuery.of(context).size.width,
          fit: BoxFit.cover,
        ),
        Scaffold(
          backgroundColor: Colors.transparent,
          appBar: AppBar(
            // Здесь мы берем значение из объекта MyHomePage, созданного методом 
            // App.build, и используем его для установки заголовка нашей навигационной панели.
            title: Text(widget.title),
            backgroundColor: Colors.transparent,
            foregroundColor: Colors.white,
          ),
          body: Center(
          ...

Ну, это выглядит довольно красиво, но фон немного раздражает из-за редактируемых элементов на экране:


Поэтому я обернул текстовые поля и изображение профиля в Card вот так, установив некоторое расстояние и отступы, чтобы это не выглядело слишком тесно:

 

Scaffold(
          backgroundColor: Colors.transparent,
          appBar: AppBar(
            title: Text(widget.title),
            backgroundColor: Colors.transparent,
            foregroundColor: Colors.white,
          ),
          body: Center(
              child: Card(
                  margin: const EdgeInsets.symmetric(horizontal: 30),
                  child: Padding(
                    padding: const EdgeInsets.all(30),
                    child: Column(
                      ...


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

Изменение иконки приложения и заголовка в меню

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

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

Для этого демонстрационного приложения я просто взял случайную иконку приложения из Google Images:


Источник: Google Images

Следуя инструкциям в readme, я определил этот минимальный набор конфигурации для успешной генерации иконок. Поместите это внизу вашего файла pubspec.yaml:

 

flutter_icons:
  android: true
  ios: true
  remove_alpha_ios: true
  image_path: "assets/app-icon.png"

Установив выше, запустите эту команду для генерации вариантов иконок, необходимых для обеих iOS и Android:

flutter pub run flutter_launcher_icons

Должны появиться файлы иконки, сгенерированные для Android и iOS соответственно в android/app/src/main/res/mipmap-hdpi/ и ios/Runner/Assets.xcassets/AppIcon.appiconset/.

К сожалению, изменение названия приложения, насколько я мог найти, по-прежнему является ручным процессом. Используя статью под названием “Как изменить название приложения в Flutter—Правильный способ в 2023 году” на Flutter Beads в качестве руководства, я изменил название приложения в следующих двух файлах для iOS и Android соответственно:

ios/Runner/Info.plist

 

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleDevelopmentRegion</key>
    <string>$(DEVELOPMENT_LANGUAGE)</string>
    <key>CFBundleDisplayName</key>
    <string>Flutter Amplify</string> <--- App Name Here

android/app/src/main/AndroidManifest.xml

 

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.flutterapp">
   <application
        android:label="Flutter Amplify" <--- App Name Here
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">

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


Заключение

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

I’m keen to get feedback on this tutorial, or any follow up tutorials people would like to see. All feedback is welcome and appreciated!

Проблемы, возникшие при работе

Отсутствие командной строки Android

Расположение моего менеджера Android SDK:

/Users/benfoster/Library/Android/sdk/tools/bin/sdkmanager

Выполнив следующее, установлены инструменты командной строки Android: Stack Overflow “Не удается установить Android SDK Java Lang Noclassdeffounderror JavaX XML Bind A.”


flutter doctor --android-licenses

Приложение остается авторизованным на iOS

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

Мой предыдущий опыт разработки для Android и использование Amplify убедили меня, что удаление приложения и повторное выполнение команды “flutter run” удалит состояние пользователя и позволит начать заново. К сожалению, даже это не дало желаемого эффекта, поэтому в итоге каждый раз, когда мне нужно было начать с чистого листа, я стираю телефон:


Source:
https://dzone.com/articles/cross-platform-mobile-app-prototyping-with-flutter-and-aws-amplify