FlutterのImagePickerで画像のキャッシュが残ってしまう問題の対策

[株式会社Never](https://neverjp.com/)のshoheiです。

株式会社Neverは「NEVER STOP CREATE 作りつづけること」をビジョンに掲げ、理想を実現するためにプロダクトを作り続ける組織です。モバイルアプリケーションの受託開発、技術支援、コンサルティングを行っております。アプリ開発のご依頼や開発面でのお困りの際はお気楽にお問合せください。

https://neverjp.com/service/

# 概要

Flutterでカメラや写真から画像を扱う際は、[image_picker](https://pub.dev/packages/image_picker)を使うかと思います。image_pickerはとても簡単に使えて便利なパッケージですが、画像のキャッシュが端末内に残ってしまい、ストレージを圧迫することがあったので、その対策をまとめます。

# `image_picker`で選択した画像の保存先

iOSとAndroidではそれぞれ異なります。

|OS  |保存先  | 保存先の取得方法|
|---|---|---|
| iOS | tmp領域<br>`/Users/xxx/Library/Developer/CoreSimulator/Devices/8453C0AB-E640-4044-87E3-7A891199062E/data/Containers/Data/Application/1B2DA1B2-CFEA-4070-8F23-F83A7B8DF6CE/tmp/` | `Directory.systemTemp` |
| Android | cache領域<br>`/data/user/0/com.example.flutter_sample/cache/` | `getTemporaryDirectory()` ※`path_provider`を利用 |

これらの領域はアプリを再起動してもファイルは残り続けます。

定期的に削除される領域のようですが[^1][^2]、削除されるまでファイルは残り続けるので、知らないうちにストレージを圧迫してしまいます。

# 対策

## `image_picker`で画像バイナリを取得後にキャッシュを削除する

`image_picker`で画像を取得すると、`XFile`形式で取得できます。これを`File`形式に変換して`Image Widget`にセットして扱うことがあると思いますが、`File`ではなく、`Uint8List`で扱った後に`File(xxx).deleteSync()`でキャッシュを削除します。

```dart
Future<void> onPickImage() async {
    final xFile = await ImagePicker().pickImage(source: ImageSource.gallery);
    if (xFile == null) {
      return;
    }
    final bytes = await xFile.readAsBytes(); // 👈 画像バイナリを取得
    setState(() {
      _imageBytes = bytes; // 👈 画像バイナリをセット
    });
    File(xFile.path).deleteSync(); // 👈 キャッシュを削除
}
```
```dart
    @override
    Widget build(BuildContext context) {
        final imageBytes = _imageBytes;
        return Scaffold(
            ...
            body: Center(
                child: imageBytes != null
                    ? Image.memory(
                        imageBytes,
                        width: 300,
                        fit: BoxFit.contain,
                      )
                    : const SizedBox.shrink(),
                ),
            ...
    }
```

ただし、画像バイナリをメモリ上に展開するので、画像を複数個扱う場合はメモリを圧迫する可能性があり注意が必要です。


## キャッシュ削除を実装

キャッシュ削除を実装し、アプリ起動時やユーザーからの操作で実行します。

```dart
void deleteFiles() {
    final list = _dir
        .listSync(recursive: true, followLinks: false)
        .whereType<File>()
        .toList();
    for (final file in list) {
        file.deleteSync();
    }
}
```

`_dir`にiOSなら`Directory.systemTemp`、Androidなら`getTemporaryDirectory`をセットします。

これらの処理をまとめた`TmpCacheDirectory`は以下の通りです。なお、ファイル数が多くなると処理が重たくなるので`compute`を使って新しいIsolateでキャッシュを削除します。

```dart
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';

class TmpCacheDirectory {
  TmpCacheDirectory._();

  static late final Directory _dir;

  static Future<void> configure() async {
    if (Platform.isIOS) {
      _dir = Directory.systemTemp;
    } else {
      _dir = await getTemporaryDirectory();
    }
  }

  static Future<void> deleteFiles() async {
    await compute<Directory, void>(
      (dir) {
        final list = dir
            .listSync(recursive: true, followLinks: false)
            .whereType<File>()
            .toList();
        for (final file in list) {
          file.deleteSync();
          debugPrint('delete: ${file.path}');
        }
      },
      _dir,
    );
  }
}
```

```dart
void main() {
    ...

    /// Delete Cache
    Future(() async {
        await TmpCacheDirectory.configure();
        await TmpCacheDirectory.deleteFiles();
    });
 
    runApp(const App());
}
```

サンプルコードは[こちら](https://github.com/never-inc/flutter_sample/blob/main/lib/pages/image_picker/)。

`tmp` `cache`領域には他のファイルも含まれる場合もあるので、画像だけを削除したい場合は、Fileパスの拡張子等から該当するファイル名だけをフィルタリングして削除してください。

もっと簡潔に消去する術はないのかな🤔

[^1]: [File System Basics - Table 1-1  Commonly used directories of an iOS app](https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html)
[^2]: [アプリ固有のファイルにアクセスする - キャッシュ ファイルを削除する](https://developer.android.com/training/data-storage/app-specific?hl=ja#internal-remove-cache)