【Android】URL から画像をダウンロードして内部ストレージに保存する方法

# はじめに

Android アプリで、URL から画像をダウンロード → 内部ストレージに保存をやりたくていろいろ調べました。記事によって書き方が多種多様で、記事どおりにしても意図した動きにならなかったりしたのですが、試行錯誤して最終的にうまくいったので備忘としてメモします。

# Android アプリでデータを保存するときの選択肢

Android アプリでデータを保存する場合、保存先として次の４つの選択肢があります。

| 保存場所 | 説明 |
|---|---|
| 内部ファイルストレージ | 端末のファイルシステムにアプリのプライベートファイルを保存。 |
| 外部ファイルストレージ | 共有外部ファイルシステムにファイルを保存。通常は写真などの共有ユーザー ファイル用。 |
| 共有の環境設定 | キーと値のペアでプライベートプリミティブデータを保存。 |
| データベース  | プライベートデータベースに構造化データを保存。 |

出典：[データとファイルのストレージの概要](https://developer.android.com/guide/topics/data/data-storage?hl=ja)

ファイルの形式や使用目的に応じてどこに保存するか考えることになりますが、今回は画像データを自分のアプリの中だけで使う目的で保存したいので、内部ファイルストレージに保存することにしました。

# 実装方法

## Glide で画像を取得

URL から画像を取得する処理は Glide を使用しますので `build.gradle` に次の２行を追加します。

```build.gradle
dependencies {
    // ...
    implementation 'com.github.bumptech.glide:glide:4.11.0'
    annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
}
```

Glide を使うことで、URL からの画像取得が非常に簡単に実現できます。

以下のとおり、まずは画像の URL を Glide に読み込ませて Bitmap 形式で取得します。このとき、バックグラウンドで実行させないとエラーが発生するので[コルーチン](https://developer.android.com/kotlin/coroutines?hl=ja)を使用します。

```MainActivity.kt
class MainActivity : AppCompatActivity() {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    
    val url = "https://xxxxxxxxxx.co.jp/path/to/image"

    GlobalScope.launch(Dispatchers.IO) {
        val bitmap = Glide.with(activity)
            .asBitmap()
            .load(url)
            .submit()
            .get()
    }
}
```

## 内部ストレージへの保存

次に取得した Bitmap ファイルを内部ストレージに保存します。

```MainActivity.kt
class MainActivity : AppCompatActivity() {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val url = "https://xxxxxxxxxx.co.jp/path/to/image"

    GlobalScope.launch(Dispatchers.IO) {
        val bitmap = Glide.with(activity)
            .asBitmap()
            .load(url)
            .submit()
            .get()
    }

    // ①
    val directory = ContextWrapper(context).getDir(
        "image", // 適当なディレクトリ名
        Context.MODE_PRIVATE
    )
    // ②
    val file = File(directory, "file_name")
    // ③
    FileOutputStream(file).use { stream ->
        bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
    }
}
```

### ①について

Android アプリには、それぞれ専用のディレクトリが用意されています。

Android Studio のメニュー `View > Tool Windows > Device File Explorer` で [Device File Explorer](https://developer.android.com/studio/debug/device-file-explorer?hl=ja) が開きますので、`data/data/パッケージ名`　ディレクトリを確認してみてください。これがアプリ専用のディレクトリとなっています。

そして ContextWrapper には、アプリ専用ディレクトリにアクセスするためのメソッドが用意されています。`getDir("image", Context.MODE_PRIVATE)` とすることで、`data/data/パッケージ名/app_image` ディレクトリへのアクセスが可能となります。ディレクトリ名は `app_` が接頭辞としてくっつきます。また、対象のディレクトリが存在しない場合は新規に作成します。

### ②について

ここでは内部ストレージのディレクトリを指定するオブジェクト File を取得しています。

ここでは `"file_name"` というファイル名を付けていますが、実際にはユニークなファイル名を付けないと１つしか保存できなくなるので注意してください。

### ③について

ここでは画像を PNG 形式で保存しています。

②で取得した File オブジェクトを引数にして、FileOutputStream を生成します。そのストリームを `compress()` メソッドの第３引数に渡すことで保存が完了します。第１引数は保存形式、第２引数はクオリティを指定しています。

また、拡張関数 `use` を使うことで、処理が終了したあとのストリームを自動的にクローズしてくれるので便利です。

## 保存した画像を確認

プログラム実行後に [Device File Explorer](https://developer.android.com/studio/debug/device-file-explorer?hl=ja) で `data/data/パッケージ名/app_image` を開くと、画像が保存されているのが確認できるかと思います（画像は `app_book_image` となってますが気になさらず）。画像が見当たらない場合は「Synchronize」を選択すると表示されるかもしれません。

![スクリーンショット 2020-09-30 22.46.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/238891/16f1d962-d060-ea21-727a-8aa224f8c8e3.png)