Flutterで様々な種類の画像を配置する方法

# 概要
Flutterで開発中に画像をアプリに配置したいということはよくある。しかし、画像と言っても様々な場所に保存されている可能性がある。よって以下の４つのパターンでの画像の配置方法をまとめる。
- アセットから読み込んで配置する方法
- 写真を撮る、または端末の写真を配置する方法
- アプリ内に保存された画像を配置する方法
- インターネット上の画像を配置する方法

そもそもFlutterで画像を取り扱うクラスはImageクラスである。Imageクラスのプロパティとして、ImageProvider\<Object>を渡す必要がある。このクラスの型が画像の種類によって変化することになる。よって以上の４つのパターンに対してImageProviderを返す関数を定義することにする。
ただし全てにおいて以下のような値を用いるが、自分でカスタマイズしてほしい。
|意味|値|
|:---:|:---:|
|ファイル名|'sample.png'|
|画像URL|'https://samples.com/sample.png'|

また作成する関数の仕様は以下のようなものである。
```dart
  Future<ImageProvider<Object>> imageBuilder(String fileName) {
    // ファイル名を受け取ってImageProviderを返す
  }
```

# アセットから読み込んで配置する方法
アセットとはアプリ本体に最初から確保される領域である。アプリの基本的なUIを構成したり、アプリのスプラッシュ画像などに用いる。まず、プロジェクトルートの下にassetsという名前のディレクトリを作る。次にassetsディレクトリの下にimagesディレクトリを作る。最後にiamgesディレクトリに配置したい画像を入れる。

```
- [project name]
    - assets
        - iamges
            - sample.png
```
次にpubspec.yaml内に以下のように記述する。

```yaml:pubspec.yaml
# The following section is specific to Flutter.
flutter:

  # To add assets to your application, add an assets section, like this:
  # assets:
  #   - images/a_dot_burr.jpeg
  #   - images/a_dot_ham.jpeg
  assets:
    - assets/images/
```
そして画像を読み込む関数を以下のようにする。
```dart
ImageProvider<Object> imageBuilder(String fileName) {
    return const AssetImage('assets/images/sample.png');
  }
```

# 写真を撮る、または端末の写真を配置する方法
写真を撮って読み込む場合にはpath providerを使う。
> https://pub.dev/packages/path_provider

ユーザが選択した画像を一旦ローカルフォルダに保存し、ローカルフォルダを参照するという一連の処理を行う。まず写真をユーザに選択してもらうための処理を作成する。
```dart
Future<void> selectImageSheetBuilder(BuildContext context) async {
    // 画像を選択するか写真を撮るか選択させる
    var result = await showModalBottomSheet<int>(
      context: context,
      builder: (BuildContext context) {
        return Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            Builder(
              builder: (context) {
                return ListTile(
                  leading: const Icon(Icons.photo),
                  title: const Text('写真を選択'),
                  onTap: () => Navigator.of(context).pop(0),
                );
              },
            ),
            Builder(builder: (context) {
              return ListTile(
                leading: const Icon(Icons.camera_alt),
                title: const Text('カメラで撮影'),
                onTap: () => Navigator.of(context).pop(1),
              );
            }),
            ListTile(
              leading: const Icon(Icons.cancel),
              title: const Text('キャンセル'),
              onTap: () => Navigator.of(context).pop(2),
            ),
          ],
        );
      },
    );
    File? file;
    if (result != null) {
      file = await getImage(result);
      if (file == null) {
        return;
      } else {
        
      }

  final int _imageSourceGallery = 0;
  final int _imageSourceCamera = 1;

   // 画像の読み込み
   Future<File?> pickImage(int imageSource) async {
    File? file;
    XFile? image;
    try {
      final ImagePicker _picker = ImagePicker();

      if (imageSource == _imageSourceGallery) {
        image = await _picker.pickImage(source: ImageSource.gallery);
      } else if (imageSource == _imageSourceCamera) {
        image = await _picker.pickImage(source: ImageSource.camera);
      } else {
        return null;
      }
    } catch (e) {
      image = null;
    }

    if (image != null) {
      file = File(image.path);
    }
    // 画像をローカルフォルダに保存
    saveImage(file);
  }
```
画像を保存する関数は次のように実装する。
```dart
  saveImage(File file) async {
    final path = await localPath;
    final imagePath = '$path/sample.png';
    File imageFile = File(imagePath);
    await imageFile.writeAsBytes(await file.readAsBytes());
  }
```
localPathとはアプリに割り当てられた保存領域のことである。iOSでは以下のような「書類とデータ」上に保存される。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2726764/33dc11ac-04ce-cd15-d285-386f410fc1f3.png" width="400" alt="iOSの書類とデータの例">

画像を取得するには、この領域に保存されたファイルにアクセスすればよい。この方法を次の章で解説する。

# アプリ内に保存された画像を配置する方法
先程保存したファイルにアクセスするための関数は以下のように実装する。
```dart
  Future<File?> getImage() async {
    try {
      final path = await localPath;
      final imagePath = '$path/sample.png';
      File imageFile = File(imagePath);
      return imageFile;
    } catch (e) {
      // ignore: avoid_print
      print('get local image: $e');
    }
    return null;
  }
```
この関数を画像をimageBuilder関数で呼び出せば良いわけである。
```dart
  Future<ImageProvider<Object>> imageBuilder() async {
      // ローカルから画像を取得
      File? file = await getImage(iconImageUrl);

      // ローカルに画像があればすぐ取得して終了
      if (file != null && file.existsSync()) {
        image = FileImage(file);
        return image;
      } else {
        // 画像が見つからなかったときの処理
      }
  }
```
# インターネット上の画像を配置する方法
画像のURLを渡せばimageProviderを返してくれるような関数がすでに実装されているので、この方法は簡単である。
```dart
  Future<ImageProvider<Object>> iconBuilderFromCloud() async {
    // インターネットから画像を取得
    return NetworkImage('https://samples.com/sample.png');
  }
```

# まとめ
Flutterで画像を扱う際にはAssetImage, FileImage, NetworkImageがあるが、全てImageProviderを継承しているので非常に扱いやすい。
よって、「ローカルファイルの画像にアクセスできればそれを表示して(FileImage)、なければクラウドから取得して(NetworkImage)、それもなければデフォルトの画像(AssetsImage)を表示する」のような処理が可能になる。
例えば、Firebase Storageなどにユーザのアイコン画像を保存しておけば、複数間デバイスで同アカウントを使用できる。しかしユーザアイコンを毎回クラウドから読み込んでいては表示に時間がかかってしまう。それを防ぐために、ローカルにも同様の画像を保存しておくことが必要である。
このように、画像の保存場所を複数作っておけばUXの改善につながるだろう。