AWS S3で追加した画像をFlutterで表示する方法

# はじめに
<!-- 発端や概要を記載 -->
`S3`で追加した画像をFlutterで表示する方法がわからず苦労したので、
解決方法を記事にしたいと思います。

今回の記事では、**S3にアクセスするのに[minio](https://pub.dev/packages/minio)というパッケージを使用しています。**

Flutterで、AWS のサービス・リソースにアクセスするときに使うライブラリは、公式には[amplify](https://aws.amazon.com/jp/amplify/)使用するのがスタンダードみたいで、`S3`へのアクセスには[amplify_storage_s3](https://pub.dev/packages/amplify_storage_s3)を使用するみたいですが、
`amplify`の事前準備が面倒に思えたので`minio`を使ってます。

また解説は**Flutterの環境設定とコードのみ**となります。
`S3`での画像追加方法については割愛しますのでご了承ください。

# 目次
<!-- タイトルとアンカー名を編集 -->
1. [設定](#設定)
1. [S3にアクセスするためのコードを実装](#S3にアクセスするためのコードを実装)
1. [S3から画像を取得し、表示するコードを実装](#S3から画像を取得し、表示するコードを実装)

<!-- 各チャプター -->
<a id="#設定"></a>
# 設定
[こちらの記事](https://zenn.dev/ohtsuki/articles/edb887a52e7f98)を参照しました。
一部重複するところがありますがご了承ください。

### 1 pubspec.yamlにパッケージを追加
以下のパッケージを追加してください
・[minio](https://pub.dev/packages/minio)
　→ S3にアクセスするために使う
・[flutter_dotenv](https://pub.dev/packages/flutter_dotenv)
　→ S3アクセスに必要な情報を定義したenvファイルを読み込むために使用する

### 2 envファイルを読み込むためにassetsを編集する
~~~yaml:pubsbec.yaml
  assets:
    - .env.dev // この行を追加
~~~

### 3 envファイルを作成する

プロジェクト直下(pubspec.yamlと同階層)に.env.devを作成する

内容は下記参照。

~~~.env.dev
END_POINT=s3-ap-northeast-1.amazonaws.com
REGION=ap-northeast-1
ACCESS_KEY=アクセスキー
SECRET_KEY=シークレットキー
BUCKET=バケット名
~~~

* アクセスキー
S3にアクセスできるIAMユーザーのアクセスキー
わからない場合、またはない場合は下記の手順で作成してください
  1. AWSのコンソールにログイン
  1. サービス一覧からIAMに移動
  1. S3にアクセスできるIAMユーザーを表示
  1. 認証情報を確認
  1. アクセスキーの作成
  1. csvをダウンロード ※外部に公開しないよう厳重に管理

* シークレットキー
上記で作成したアクセスキーのCSVに載ってます

* バケット名
オブジェクト(画像ファイル)を追加したバケットの名前

### 4 envファイルをGit管理から外す
**※ソースをGit管理している人は.gitignoreに下記を追加してください
ソースをPushする際には、作成したenvファイルがコミットに含まれていないか確認お願いします
認証情報が外部に漏れる可能性があるため、Gitには上げない方がいいと思います**

~~~.gitignore
.env.dev
.env
~~~
<a id="#S3にアクセスするためのコードを実装"></a>
# S3にアクセスするためのコードを実装
### 1 envファイルを読み込み
`flutter_dotenv`をインポートし、`void main()`のメソッド内の`runApp`の前に以下の一行を追加してください。
~~~dart:main.dart
import 'package:flutter_dotenv/flutter_dotenv.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  // 下記の一行を追加
  await dotenv.load(fileName: ".env.dev");
  runApp(const ProviderScope(child: MyApp()));
}
~~~
### 2 S3を操作するMinioを取得するコードを実装
Minioを取得するシングルトンのクラスとして実装してみました。
~~~dart:S3.dart
class S3 {
  Minio? _minio;
  S3._();

  static final instance = S3._();
  Minio getMinio(){
      _minio ??= Minio(
        endPoint: dotenv.env['END_POINT']!,
        region: dotenv.env['REGION']!,
        accessKey: dotenv.env['ACCESS_KEY']!,
        secretKey: dotenv.env['SECRET_KEY']!,
        useSSL: true,
      );
    return _minio!;
  }
}
~~~

<a id="#S3から画像を取得し、表示するコードを実装"></a>
# S3から画像を取得し、表示するコードを実装
本記事の肝の部分です。

### 1 S3から画像を取得し表示するWidget
Widget化してみましたので、適宜カスタマイズしてお使いください。
デバッグはあまりしてないので、不具合等あるかもしれませんがご了承ください。
あと、`main.dart`のインポート部分は、[S3にアクセスするためのコードを実装](#S3にアクセスするためのコードを実装)の**2 S3を操作するMinioを取得するコードを実装**で作成したクラスを定義したファイルをインポートしてください。(私は横着してmain.dartに実装してます)

使い方は、画像表示したい箇所で`ImageFromS3(bucketName: 'バケット名', objectName: 'オブジェクト名')`を呼んでください。

~~~dart:image_from_s3.dart
import 'dart:typed_data';
import 'package:flutter/material.dart';
// S3クラスを定義したファイルをインポート
import '../main.dart';

class ImageFromS3 extends StatelessWidget {
  final String bucketName;
  final String objectName;
  const ImageFromS3({Key? key, required this.bucketName, required this.objectName}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
        future: getImage(),
        builder: (BuildContext context, AsyncSnapshot<Image> snapshot) {
          if (snapshot.connectionState != ConnectionState.done) {
            return const CircularProgressIndicator();
          }

          if (snapshot.hasError) {
            return Text(snapshot.error.toString());
          }

          if (snapshot.hasData) {
            return Container(child: snapshot.data!,);
          } else {
            return const Text("データが存在しません");
          }

        },
    );
  }

  Future<Image> getImage() async {
    final minio = S3.instance.getMinio();
    final stream = await minio.getObject(bucketName, objectName);
    List<int> memory = [];
    await for (var value in stream) {
      memory.addAll(value);
    }
    return Image.memory(Uint8List.fromList(memory));
  }
}
~~~

### 2 画像を取得し、表示する部分の解説
画像を取得し、Imageウィジェット化している部分は上記コードの`getImage`メソッドです。

まず以下のコードでS3からオブジェクトを取得しています
```dart
    final minio = S3.instance.getMinio();
    final stream = await minio.getObject(bucketName, objectName);
```

この部分は他の記事でも解説があり、少し調べればたどり着けました。

続いて、取得したオブジェクトをImageウィジェット化するコードが以下です。
(私はこの部分の実装がわからず苦労しました)
```dart
    List<int> memory = [];
    await for (var value in stream) {
      memory.addAll(value);
    }
    return Image.memory(Uint8List.fromList(memory));
```

まず、`minio.getObject`メソッドでバイトデータのMinioByteStreamが返ってくるので、
バイトデータから`Imageウィジェット`を作成するために、`Imageウィジェット`を作成するメソッドの一つである`Image.memory`を使えば画像を表示できるのでは？と当りを付けました。

ただ、`MinioByteStream`をどうやってバイトデータに変換すればいいのかわからず、`Stream`について調べたところ、
`await for`を使えば、`Stream`の終了を待てるという記事([dart の stream を理解して async* と yield を正しく使う](https://qiita.com/shindex/items/1ec40cc224aedcf31946))があったので、
試しに実装してみたところうまく表示できました。

少しでもご参考になりましたら、いいね押していただけると励みになります( ｀・∀・´)ﾉﾖﾛｼｸ