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.

このチュートリアルに従っていけば、それほど時間を要さずに1時間程度で終わるはずです。私は様々な問題と戦いながら何時間もかかりましたが、十分にドキュメント化してあるので、それらに遭遇することはないと思います。

こちらが完成品です。「これは前に作ったもの」版をご希望の場合は、READMEの手順に従って、約15分で稼働させることができるでしょう。こちらがGitHubリンクです。

このチュートリアルは5つの部分で構成されています:

  1. 前提条件とコードベースのセットアップ
  2. 認証の追加
  3. プロフィール画像のアップロード
  4. ユーザー詳細の保存
  5. デザインの装飾の追加

推奨

Flutterは数年にわたり非常に成熟したプラットフォームであり、活発なコミュニティと多くのプラグインや拡張機能があり、ほとんどのことを達成できます。

Amplifyも強力なプラットフォームですが、API機能は扱いにくく、FlutterライブラリはAmplifyの最新の発表や機能と同期していなかったことがわかりました。特に、AppSync GraphQLとDataStore(オフラインデータストアおよび同期)ではかなり脆弱でした(後ほど見ていくことになります)。

この2つを組み合わせると、モバイルアプリプロトタイプの開発を加速するのに素晴らしい組み合わせですが、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は73個のファイルを作成しました。これらが何であるか見てみましょう:


私たちが最も時間を費やすフォルダはios/androidlib/です。iosandroidフォルダ内には、それぞれXCodeとAndroid Studioで開くことができるプロジェクトリソースがあります。これらのプロジェクトは、プラットフォームに依存しないDartコードとターゲットプラットフォームのインターポータビリティを果たします。それでは、iOSで試してみましょう:

iOSセットアップ

 

open -a Simulator
flutter run

私のMacでは、最小限のXCodeセットアップで、スケルトン化されたFlutterアプリが実行されているiPhone 14 Pro Maxシミュレーターを実行するまで何もなかった状態からすぐに完了しました。これはかなり素晴らしいです。


次のように表示された場合は、おめでとうございます。スケルトンを正常に生成できました。


また、ios/Runner.xcodeprojプロジェクトをXCodeで開き、その内容を探索し、シミュレーターや物理デバイスで実行することもできます。

Androidセットアップ

Androidは少し直感的ではありません。Android Studioでエミュレーターを明示的に設定するまで実行できません。最初にAndroid Studioでandroid/flutterapp_android.imlプロジェクトを開き、アプリをテストするためにエミュレーターを設定して実行できます。


Android Studioに数分の時間を与えてください。Gradleとアプリを実行するために必要なすべての依存関係をダウンロードします。この進行状況を右下隅のプログレスバーで追跡できます。

Android Studioが落ち着いたら、AVDで既にシミュレートされたデバイスを設定している場合は、ウィンドウの右上にある再生ボタンを押すことができます。


そして、見よ、Androidで同じアプリが表示されます。


これは、新しいFlutterプロジェクトを作成する際に提供されるサンプルアプリコードをデモンストレーションしています。このチュートリアルの過程で、このコードを徐々に独自のものに置き換えていきます。

これは、Flutter開発の基盤が整った今、gitコミットを行う良いタイミングです。これで、Flutterコードをいじり始めて、iOSとAndroidで同時に結果を見ることができるようになりました。

Flutterは、AndroidとiOSの間の中間言語としてDartを使用し、あなたが対話するすべてのコードはlib/フォルダ内に存在します。main.dartファイルがあるはずで、ここからいじり始めます。

Amplifyを使用して新しいアプリを構成してデプロイする

これでモバイルアプリツールが作業に対応できるようになりましたが、アプリの機能をサポートするためのバックエンドインフラストラクチャが必要です。

AWSとその多数のサービスを使用してアプリをサポートしますが、すべてAWS Amplifyサービスを使用して管理されます。それらの多くは私たちに対して透明に処理され、どのサービスを利用するかを心配する代わりに、どの機能をデプロイするかに焦点を当てます。

始めに、コードフォルダ内で次のコマンドを実行してください。

 
amplify init

このコマンドは、プロジェクト内でAWS Amplifyを初期化します。初めて使用する場合、いくつかの質問をしてもらいます。プロジェクトに協力する後続の人々にとって、このコマンドを実行すると、既存のAmplify設定でローカル環境を設定します。



これにより、設定とAmplifyアプリの状態を保存するための初期AWSリソースが提供されます。具体的には、S3バケットです。

上記のデプロイメントの進行状況バーとステータスは、一部の人には馴染み深いものかもしれません。CloudFormationであり、AWS CDKと同様に、AmplifyはCFNを内部的に使用して、必要なすべてのリソースをプロビジョニングします。CloudFormationスタックコン�soleを開くことで、その動作を確認できます。


最後に、CLIが完了すると、以下に似た確認が表示され、新しくデプロイされたアプリをAmplify Consoleで確認できるようになります。



環境管理

AWS Amplifyには「環境」という概念があり、これはアプリケーションとリソースの隔離されたデプロイメントです。歴史的に、環境の概念は、あなたが持っていたエコシステム内で作成されなければなりませんでした(例:CloudFormation、CDK)、名前付け規則やパラメーターを使用します。Amplifyでは、それは第一級市民であり、共有環境のプロビジョニングや変更のプロモーション(例:Dev > QA > PreProd > Prod)、開発者や機能ブランチごとの環境など、複数の環境を持つことができます。

Amplifyはまた、Amplifyホスティングアドオンを使用してCI/CDサービスを構成およびプロビジョニングし、アプリケーションに統合して、エンドツーエンドの開発エコシステムを提供することができます。これにより、CodeCommit、CodeBuild、およびCodeDeployがソース管理、ビルド、およびアプリケーションのデプロイメントを処理するように設定されます。このチュートリアルではこれについては扱いませんが、ビルド、テスト、リリースの公開を自動化するために使用できます。

第二部: 認証の追加

通常、AWSの認証サービスであるCognitoやIAMなどのサポートサービスについて学び、CloudFormation、Terraform、またはCDKのようなものを使用してすべてを組み合わせる必要があります。Amplifyでは、次のように簡単です。

 
amplify add auth

Amplify addを使用すると、プロジェクトにさまざまな「機能」を追加できます。Amplifyは、CloudFormationを使用して必要なすべてのサービスをデプロイおよび構成します。そのため、アプリの機能に集中し、配管について心配することを少なくできます。

上記の3つの魔法の言葉を入力するだけで簡単だと言っても…それほど簡単ではありません。Amplifyは、ユーザーの認証方法や設定したい制御について理解するためにさまざまな質問をします。「デフォルトの設定」を選択すると、Amplifyは理にかなったデフォルトで認証を設定し、すぐに開始できるようにします。「手動設定」を選択して、Amplifyがどれほどカスタマイズ可能かを実証します。


上記の設定により、メールアドレスを必要とせずに携帯電話番号だけでアカウントを作成できるようになり、MFAを使用して本人確認とさらなるサインオン試行を検証します。OAuthを標準化された認証メカニズムとして強くお勧めしますが、ここでは簡略化のために使用していません。

機能を追加する際、それらはすぐにプロビジョニングされません。だからこそ、コマンドが不気味に迅速に完了します。これらのコマンドは、これらの機能をデプロイするためにAmplifyアプリの設定(およびローカル環境)を準備するだけです。

機能(または設定の変更)をデプロイするには、プッシュする必要があります。

 
amplify push

注意: これは、バックエンドとフロントエンドの両方のサービスをビルドおよびデプロイするamplify publishコマンドとは異なります。プッシュはバックエンドリソースのみをプロビジョニングします(このチュートリアルではモバイルアプリを構築する予定なので、それが必要なことだけです)。

認証(または任意のAmplify機能)を追加すると、Amplifyはlib/amplifyconfiguration.dartという名前のdartファイルを追加します。このgitは無視されます。なぜなら、それはデプロイされたリソースに関連する機密情報を含んでおり、Amplify環境と自動的に同期されるためです。詳細についてはこちらをご覧ください。

この時点で、Amplifyはアプリと開発環境を設定し、認証のためにCognitoを構成しています。それに従っているなら、この時点に戻す必要がある場合に備えて、gitコミットを行うのが良いでしょう。Amplifyは既に不要なファイルを除外するための.gitignoreファイルを作成してくれているはずです。

バックエンド認証インフラストラクチャが整ったので、Flutterでモバイルアプリの構築を開始できます。

アプリ内でのユーザー認証

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

これはamplify_flutterに含まれる標準の認証UI画面とワークフローを使用しています。Amplify Flutterの依存関係を追加するには、pubspec.yamlファイル内の“dependencies”の下に以下を追加してください。

 

amplify_flutter: ^0.6.0
amplify_auth_cognito: ^0.6.0

VSCode内のFlutterおよびDart拡張機能を使用していない場合(またはVSCodeを使用していない場合)、flutter pub getコマンドを実行する必要があります。そうでない場合は、VSCodeがpubspec.yamlファイルを保存する際に自動的にこれを実行します。

認証を統合するためのクイックスタートのアプローチがあり、事前に作成されたAuthenticator UIライブラリを使用して、後でカスタマイズできるサインインフローを迅速にブートストラップできます。このチュートリアルでは、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に変更してから、
          // "hot reload"を呼び出してください("flutter run"を実行したコンソールで"r"を押して、
          // またはFlutter IDEで単に変更を保存して"hot reload"を実行してください)。
          // カウンターがゼロにリセットされなかったことに注意してください。アプリケーションは
          // 再起動されていません。
          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でUIレイアウトとコンポーネントを構成するために使用される基本的な構成要素です。Flutterのレイアウトのほとんどすべてがウィジェットであり、列、スカフォルディング、パディングとスタイリング、複雑なコンポーネントなどです。Flutterのスタートドキュメントの例では、「Center」ウィジェットに続いて「Text」ウィジェットを使用して、中央に配置されたテキストを表示しています。このテキストは「Hello World」と表示されます。

上記のコードは、MyHomePageウィジェットを認証ウィジェットで装飾し、AmplifyAuthCognitoプラグインを追加し、前のamplify add authコマンドがlib/amplifyconfiguration.dartに生成した設定を使用して、AWS Cognito User Poolに自動的に接続します。

Flutterを実行し、認証統合のデモンストレーションを行う際、私は「Running pod install」のステップが完了するまでしばらく時間がかかりました(ほぼ5分)。ぐっと我慢してください。


認証の変更が行われ、アプリが起動すると、基本的ですが機能的なログイン画面が表示されます。



アカウントを作成」の流れで、電話番号とパスワードを提供し、その後登録を完了するためのMFAチャレンジが表示されます。そして、そのユーザーがCognitoユーザープール内に作成されていることがわかります:


これを簡単にテストできます。仮想Androidデバイスでも十分です。FlutterとDartプラグインをインストールしていれば、VSCodeを離れる必要はありませんし、Android Studioを開く必要もありません。VSCodeの右下隅にある現在アクティブなデバイス(iPhone)の名前を選択し、既に作成した仮想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: 独自のアプリケーションIDを指定してください(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ウィジェットを使用していくつかのウィジェットをスキャフォルドしています。以前は、これを認証ウィジェットで装飾しました。今回は、MaterialAppのMyHomePage子ウィジェットを拡張してプロフィール画像を提供します。

ウィジェットを木構造で組み合わせ、これを「ウィジェット階層」と呼びます。常にトップレベルのウィジェットから始めます。私たちのアプリでは、初期サインインを処理する認証ラッパーウィジェットがそれにあたります。Scaffoldはレイアウトの基盤として良いウィジェットです。マテリアルアプリでよくトップレベルのウィジェットとして使用され、フローティングアクションボタンやボトムシート(詳細情報をスワイプアップするためのもの)、アプリバーなど、いくつかのプレースホルダがあります。

まずは、ネットワーク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),
              ),
            )
          ],

これで、Webから画像を表示できます。


次に、ユーザーが自分のデバイス上の画像からプロフィール画像を選択できるようにします。少しの検索で、画像を選択する詳細を抽象化したこのライブラリを見つけました:

  • Google: “Pub Dev Packages Image Picker”

ユーザーに画像を選択するように促すために必要なのは、2行のコードだけです:

 

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

iOSの場合、ユーザーの写真を表示するアクセスを要求するために、NSPhotoLibraryUsageDescriptionキーを<プロジェクトルート>/ios/Runner/Info.plist Xcode構成ファイルに追加する必要があります。そうしないと、アプリがクラッシュします。

これを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");
  }

Flutterに渡されたLambda内のウィジェットフィールドを更新するためにsetState()を呼び出し、build()関数を呼び出す必要があることを知らせます。ここで、更新された状態を使用してウィジェットを再描画できます。この場合、プロフィール画像が表示されるように、画像を表示するコンテナウィジェットを作成します。null非許容??演算子は、ユーザーがまだ画像を選択していない場合にデフォルトのプロフィールプレースホルダーを提供します。

また、リポジトリにプロフィールプレースホルダー画像を追加し、ビルドで拾われるようにpubspec.ymlファイルに参照を追加する必要があります。私のリポジトリの画像を使用し、pubspec.ymlファイルにこれを追加してください:

 

# 次のセクションはFlutterパッケージに特有です。
flutter:
...

  # アプリケーションにアセットを追加するには、次のようにアセットセクションを追加します:
  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はAppSync内のデータと対話するための高レベルのAPIを提供しますが、このチュートリアルでは、グローバルセカンダリインデックスを使用してテーブルスキャンを回避できるようにするため、グラフQLと低レベルのクエリを使用します。ここまでどうやって来たのか、そしてどんな落とし穴があるのか理解したい場合は、「AWS AmplifyとAppsyncをFlutterで動作させて調整する際の課題」をチェックしてください。


AmplifyはAPIを作成する際に尋ねる質問をデフォルトで設定しようとしますが、変更したいオプションにスクロールアップして上書きできます。このシナリオでは、DataStoreを活用するためにGraphQLエンドポイントが必要であり、APIの認証はCognitoユーザープールによって処理されるべきです。ファイングレインアクセス制御を適用して、ユーザー自身の詳細のみを更新または削除できるようにするためです(他のユーザーはそれらを表示できます)。

APIを作成する際、Amplifyは基本的なToDo GraphQLスキーマタイプを作成します。これを更新し、APIの変更をプッシュする前にいくつかの認証ルールを追加します。

“ToDo”テンプレートのGraphQLスキーマを、ユーザープロフィール情報に対応できるように変更します。

 

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
}

プライベートルールにより、ログインしているユーザーは他の誰かのプロフィールを閲覧できます。「public」を使用しないことで、ログインしていない人々がプロフィールを閲覧できないようにしています。IAMプロバイダーは、ユーザーが直接GraphQL APIにアクセスすることを防ぎます。彼らはアプリを使用し、Cognito IDプール内の「unauthenticated」ロールを使用する必要があります(つまり、ログアウトしている状態)ユーザー詳細を閲覧するために。

「owner」ルールは、ユーザーが自分のプロフィールを作成、読み取り、更新できるようにします。この例では、彼らが自分のプロフィールを削除できないようにしています。

この時点で、API機能をサポートするクラウドインフラストラクチャをプロビジョニングできます。

 
amplify push

既存のGraphQLモデルをToDoからUserProfileに変更する場合、以前にamplify pushを実行し、インフラストラクチャをプロビジョニングしていると、既存のDynamoDBテーブルを破棄することを要求される変更がある場合、データの損失を防ぐためにAmplifyがこれを防止するエラーが表示されることがあります。このエラーが発生した場合は、amplify push --allow-destructive-graphql-schema-updatesを実行する必要があります。

amplify pushを実行すると、AmplifyとCloudFormationはAppSync GraphQL API、中間リゾルバー、およびこのようなバッキングDynamoDBテーブルを立ち上げます。



GraphQLスキーマを定義した後、Amplifyを使用して、APIで動作するモデルおよびリポジトリ層を表すDartコードを生成できます:

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

次に、ユーザーが「Save」フローティングアクションボタンを押したときに変更がDynamoDBと同期されるように、TextFieldをAppSync GraphQL APIに接続します。

 

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 ?? "";
      });
    }
  }

これで、Cognitoでセキュリティ保護され、DynamoDBがバックアップされたAPIを使用してデータを保存できるようになりました。インフラストラクチャコードを書かなくてもかなり素晴らしいですね。

この時点で、ユーザーのプロファイル情報を照会、表示、更新する手段があります。私にはセーブポイントのように感じます。

パート5:デザインの魅力を加える

最後に、拡張したサンプルアプリは少し地味です。少し命を吹き込む時間です。

私は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(
            // ここでは、App.build メソッドによって作成された MyHomePage オブジェクトから値を取り出し、それをアプバーのタイトルに設定します。
            // App.build メソッドによって作成された MyHomePage オブジェクトから値を取り出し、それをアプバーのタイトルに設定します。
            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(
                      ...


これはその一例ですが、マテリアルデザインシステムを利用したより慣用的な方法があるのではないかと思います。多分別の投稿のテーマでしょう。

アプリのアイコンとタイトルをメニューで変更

アプリのアイコンを変更したい場合は、iOS と Android のそれぞれに対して異なる解像度のバリエーションのロゴを複数提供する必要があります。両方とも別々の要件があります(アプリの承認を妨げるためにいくつか無視することになります)ので、すぐに手間のかかる作業になります。

ありがたいことに、アイコンのソース画像を与えると、両方のプラットフォームで必要なすべてのバリエーションを生成できる Dart パッケージがあります。

このデモアプリでは、Google 画像からランダムなアプリアイコンをナッパしました:


出典: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/に生成されるはずです。

アプリの名前を変更する方法は、残念ながらまだ手作業のプロセスです。「How to Change App Name in Flutter—The Right Way in 2023」というFlutter Beadsの記事を参考に、iOSおよびAndroidのそれぞれの次の2つのファイルでアプリ名を変更しました:

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「Failed to Install 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