AWS の開始方法

Flutter アプリケーションを構築する

AWS Amplify を使用してシンプルな Flutter アプリケーションを作成する

モジュール 4: 画像保存機能を追加する

このモジュールでは、ストレージと、画像をアプリのメモに関連付ける機能を追加します。

はじめに

認証されたユーザーだけがこのアプリに入ることができるので、写真を撮影してアプリの Amazon S3 バケット内の非公開フォルダにアップロードすることをユーザーに許可できます。

このモジュールでは、お使いの Amplify アプリにストレージカテゴリを追加し、デバイスのカメラで撮影した写真をアップロードして、個々のユーザーに関連付けられたすべての写真をダウンロードおよび表示します。

学習内容

  • ストレージカテゴリの設定
  • Amazon S3 へのファイルのアップロード
  • Amazon S3 からのファイルのダウンロード
  • URL からの画像の表示とキャッシュ

主要な概念

ストレージ - ストレージの概念は、ファイルを特定の場所に保存して、それらのファイルを必要な時に取り出せることです。このケースでは、Amazon S3 に画像を保存し、そこから画像をダウンロードすることです。

 所要時間

20 分

 使用するサービス

実装

  • ストレージサービスを作成する

    プロジェクトのルートディレクトリで次のコマンドをターミナルに入力して、ストレージサービスを Amplify プロジェクトに追加します。

    amplify add storage

    他のカテゴリと同様に、Amplify CLI はストレージサービスの設定方法に関する質問を表示します。Enter キーを押して、ほとんどの質問にデフォルトの回答で答えます。

    ➜  photo_gallery git:(master) amplify add storage
    ? Please select from one of the below mentioned services: Content (Images, audio
    , video, etc.)
    
    ? Please provide a friendly name for your resource that will be used to label th
    is category in the project: s33daafe54
    ? Please provide bucket name: photogalleryf3fb7bda3f5d47939322aa3899275aab
    ? Who should have access: Auth users only

    認証済みユーザーに必要なアクセス権限の種類を指定する場合は、キーを押して「create/update」、「read」、「delete」を選択します。

    ? What kind of access do you want for Authenticated users? create/update, read,
    delete

    次に、ストレージリソースが完全に設定されるまでデフォルトの回答の入力を続けます。

    ? Do you want to add a Lambda Trigger for your S3 Bucket? No
    Successfully added resource s33daafe54 locally

    今度は、同期を維持するため設定済みのストレージリソースをバックエンドに送る必要があります。次のコマンドを実行します。

    amplify push

    Amplify CLI は実行される変更のステータスレポートを提供します。

    ✔ Successfully pulled backend environment dev from the cloud.
    
    Current Environment: dev
    
    | Category | Resource name        | Operation | Provider plugin   |
    | -------- | -------------------- | --------- | ----------------- |
    | Storage  | s33daafe54           | Create    | awscloudformation |
    | Auth     | photogallery42b5391b | No Change | awscloudformation |
    ? Are you sure you want to continue? (y/n)

    ストレージカテゴリが作成中であることと、前回のモジュールの設定から認証には何も変更がないことが示されます。

    ストレージリソースの設定がバックエンドで完了すると、成功したことを示す出力が表示されます。

    ✔ All resources are updated in the cloud
  • 依存関係をインストールする

    次に、pubspec.yaml ファイルを Visual Studio Code で開き、ストレージプラグインを依存関係として追加します。

    ... # amplify_auth_cognito: '<1.0.0'
    
    amplify_storage_s3: '<1.0.0'
    
    ... # dev_dependencies:

    このファイルを保存して、Visual Studio Code に Amplify Auth Cognito プラグインをインストールさせます。保存時に依存関係がインストールされない場合は、ターミナルから「$ flutter pub get」を実行することもできます。

    次のような出力が得られるはずです。

    exit code 0
  • プラグインを設定する

    ストレージを Amplify のインスタンスにプラグインとして追加できるように、main.dart に戻ります。

    ... // void _configureAmplify() async {
    
    _amplify.addPlugin(
        authPlugins: [AmplifyAuthCognito()],
        storagePlugins: [AmplifyStorageS3()]);
    
    ... // try {

    アプリを実行します。Amplify が今も適切に設定されストレージプラグインを含んでいることを示す成功メッセージがログに表示されるはずです。

    flutter: Successfully configured Amplify 🎉
  • 機能を実装する

    コードを整理された状態にしておくため、ファイルのアップロードおよびダウンロード用のロジックをカプセル化する「storage_service.dart」という名前の独立したファイルを作成しましょう。以下のコードを追加します。

    import 'dart:async';
    import 'package:amplify_core/amplify_core.dart';
    import 'package:amplify_storage_s3/amplify_storage_s3.dart';
    
    class StorageService {
      // 1
      final imageUrlsController = StreamController<List<String>>();
    
      // 2
      void getImages() async {
        try {
          // 3
          final listOptions =
              S3ListOptions(accessLevel: StorageAccessLevel.private);
          // 4
          final result = await Amplify.Storage.list(options: listOptions);
    
          // 5
          final getUrlOptions =
              GetUrlOptions(accessLevel: StorageAccessLevel.private);
          // 6
          final List<String> imageUrls =
              await Future.wait(result.items.map((item) async {
            final urlResult =
                await Amplify.Storage.getUrl(key: item.key, options: getUrlOptions);
            return urlResult.url;
          }));
    
          // 7
          imageUrlsController.add(imageUrls);
        
        // 8
        } catch (e) {
          print('Storage List error - $e');
        }
      }
    }
    1. まず、Amazon S3 から取得される画像の URL を管理する StreamController を初期化します。
    2. この関数は、GalleryPage に表示する必要のある画像を取得するプロセスを開始します。
    3. ユーザーがアップロードした写真を表示したいだけなので、アクセスレベルを StorageAccessLevel.private に指定し、ユーザーの非公開の写真が確実に非公開のまま保持されるようにします。
    4. 次に、S3ListOptions で取得した関連するすべての写真をリスト化するよう、ストレージにリクエストします。
    5. リスト化がうまくいった場合、そのリスト化の結果が含んでいるのは実際の写真の URL ではなくキーのリストのみであるため、各写真の実際のダウンロード URL を取得する必要があります。
    6. .map を使用してリスト化の結果内の各アイテムを反復処理し、非同期的に各アイテムのダウンロード URL を返すようにします。
    7. 最後に、この URL リストをストリームに送信し、確認します。
    8. 途中でエラーが発生した場合は、単純にエラーを出力します。

    iOS 上では、アプリが確実に画像をダウンロードできるよう Info.plist (ios > Runner > Info.plist) で App Transport Security を更新する必要があります。

    ... <!-- <string>Need to take pictures</string> -->
    
    <key>NSAppTransportSecurity</key>
    <dict>
       <key>NSAllowsArbitraryLoads</key>
       <true/>
    </dict>
    
    ... <!-- </dict> -->

    何もアップロードしていない場合は、S3 の画像 URL リストを作成することには意味がないため、画像をアップロードする関数を追加しましょう。

    // 1
    void uploadImageAtPath(String imagePath) async {
     final imageFile = File(imagePath);
     // 2
     final imageKey = '${DateTime.now().millisecondsSinceEpoch}.jpg';
    
     try {
       // 3
       final options = S3UploadFileOptions(
           accessLevel: StorageAccessLevel.private);
    
       // 4
       await Amplify.Storage.uploadFile(
           local: imageFile, key: imageKey, options: options);
    
       // 5
       getImages();
     } catch (e) {
       print('upload error - $e');
     }
    }
    1. これは、カメラにより提供された画像のパスを取得する非同期関数です。
    2. 写真が確実に一意のキーを持つようにするため、タイムスタンプをキーとして使用します。
    3. 前述したように、getImages を実装する際、S3 バケット内のユーザー自身のフォルダに画像がアップロードされるように、アクセスレベルを StorageAccessLevel.private に指定する必要があります。
    4. 次に、そのキーとファイルアップロードのオプションを指定してファイルをアップロードします。
      最後に、getImages を呼び出して画像 URL の最新リストを取得し、ストリームに送信します。
    5. アップロードとダウンロードを正しく行うために必要なすべてのコーディングを完了しました。今度はすべてをつなぎ合わせてテストする必要があります。

    まず、StreamController を引数として受け取ることでストレージから取り出された画像の URL を確認できるよう、GalleryPage を更新しましょう。

    ... // class GalleryPage extends StatelessWidget {
    
    final StreamController<List<String>> imageUrlsController;
    
    ... // final VoidCallback shouldLogOut;
    ... // final VoidCallback shouldShowCamera;
    
    GalleryPage(
       {Key key,
       this.imageUrlsController,
       this.shouldLogOut,
       this.shouldShowCamera})
        : super(key: key);
    
    ... // @override

    次に、単に GridView.builder の代わりに StreamBuilder を返すように _galleryGrid を更新します。

    Widget _galleryGrid() {
     return StreamBuilder(
         // 1
         stream: imageUrlsController.stream,
         builder: (context, snapshot) {
           // 2
           if (snapshot.hasData) {
             // 3
             if (snapshot.data.length != 0) {
               return GridView.builder(
                   gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                       crossAxisCount: 2),
                   // 4
                   itemCount: snapshot.data.length,
                   itemBuilder: (context, index) {
                     return CachedNetworkImage(
                       imageUrl: snapshot.data[index],
                       fit: BoxFit.cover,
                       placeholder: (context, url) => Container(
                           alignment: Alignment.center,
                           child: CircularProgressIndicator()),
                     );
                   });
             } else {
               // 5
               return Center(child: Text('No images to display.'));
             }
           } else {
             // 6
             return Center(child: CircularProgressIndicator());
           }
         });
    }
    1. StreamBuilder は、データのスナップショットを提供するために StorageService から渡される imageUrlsController を使用します。
    2. この UI では、スナップショットにユーザーに関連する表示データがあることが求められます。
    3. また、そのデータに実際に項目があるか判定することも必要です。データがある場合、GridView の構築を続行します。
    4. ハードコードされた数字を使用する代わりに、スナップショットのデータの長さに基づいた GridView サイズを作成できるようになりました。
    5. スナップショットに何も項目がない場合、表示するものがないことを示すテキストを表示します。

    今はまだグリッドの各項目のプレースホルダーが表示されています。ストリームによって提供される URL から各画像をダウンロードすることが必要になります。簡単に行えるようにするため、pubspec.yaml に新しい依存関係を追加します。

    ... # amplify_storage_s3: '<1.0.0'
    
    cached_network_image: ^2.3.3
    
    ... # dev_dependencies:

    このライブラリでは、URL を受け取るだけで画像をダウンロードおよびキャッシュできるウィジェットを利用できます。変更を保存し、プレースホルダーを置換できるよう GalleryPage に戻ります。

    ... // itemBuilder: (context, index) {
    
    return CachedNetworkImage(
      imageUrl: snapshot.data[index],
      fit: BoxFit.cover,
      placeholder: (context, url) => Container(
          alignment: Alignment.center,
          child: CircularProgressIndicator()),
    );
    
    ... // itemBuilder closing });

    プレースホルダーは CachedNetworkImage で置換されました。CachedNetworkImage はスナップショットからの URL を渡され、itemBuilder をとおしてインデックス化されます。画像を読み込む間、ウィジェットは CircularProgressIndicator を表示します。

    今度は GalleryPage と CameraPage を _CameraFlowState の StorageService に接続します。まず、StorageService のインスタンスを保持するプロパティを作成します。

    ... // bool _shouldShowCamera = false;
    
    StorageService _storageService;
    
    ... // List<MaterialPage> get _pages {

    次に、initState メソッドで _storageService を初期化します。

    ... // _getCamera();
    
    _storageService = StorageService();
    _storageService.getImages();
    
    ... // initState closing }

    StorageService を初期化後すぐに、アップロードされた画像が取り出されるよう getImages を呼び出すことができます。

    それでは、StreamController を GalleryPage に渡しましょう。

    ... // child: GalleryPage(
        
    imageUrlsController: _storageService.imageUrlsController,
    
    ... // shouldLogOut: widget.shouldLogOut,

    最後に、写真が撮影されたら CameraPage の didProvideImagePath の機能を更新します。

    ... // this._toggleCameraOpen(false);
    
    this._storageService.uploadImageAtPath(imagePath);
    
    ... // didProvideImagePath closing }

    これで完了です。 アプリで写真を撮影して S3 にアップロードする準備ができました。

    構築と実行をお試しください。

    EndofModule4-gif

まとめ

すばらしいです。 このアプリの中核となる機能が実装され、ユーザーは写真を撮影することができます。撮影された写真はアプリの S3 バケットの非公開セクションに保存されます。

次のモジュールでは、分析を追加します。

このモジュールは役に立ちましたか?

ありがとうございます
このチュートリアルで良かった点をお聞かせください。
閉じる
ご期待に添えず申し訳ありません
古い説明、わかりにくい説明、間違った説明はございませんでしたか? このチュートリアルの改善のために、ぜひフィードバックをお寄せください。
閉じる