Android のファイル操作に Okio を使う

# 概要
OkHttp の依存に入っている [Okio](https://github.com/square/okio) というライブラリはご存知でしょうか？このライブラリ、OkHttp のおまけで済ますにはもったいないので、ぜひ使ってみましょう。

## [Okio](https://github.com/square/okio) とは？
アメリカ合衆国の [Square, Inc.](https://ja.wikipedia.org/wiki/Square,_Inc.) が開発した I/O ライブラリです。```java.io``` と ```java.nio``` の機能を使いやすくするライブラリとして作られています。Java で実装されています。

## 背景
Java 6 互換の開発環境では、機能が十分とは言えない File クラスを使う必要があり、
特にファイルを読み書きする処理は、昔の Java の API しか使えない環境ですと割と煩雑になりがちです。自作のファイルユーティリティクラスを作っているプロジェクトもあるかと思います。そうした状況であれば、この Okio を採用して車輪の再発明を回避することが可能です。

Android SDK の依存はないので、Java 6 互換の普通の Java ライブラリとしても使えます。
ですが、通常の Java アプリケーション開発では NIO2 や Apache の [Commons IO](https://commons.apache.org/proper/commons-io/) に含まれる [FileUtils](https://commons.apache.org/proper/commons-io/javadocs/api-2.5/org/apache/commons/io/FileUtils.html) がありますので、Java 6 でないといけない環境でない限り、こちらを使うことはないかと思います。

## ライセンス
記事を書いている時点では Apache 2.0 で提供されています。アプリで利用する場合はアプリ内にライセンス表記が必要です。

https://github.com/square/okio/blob/okio-parent-1.13.0/LICENSE.txt

----
# 導入
記事を書いている時点での最新は [1.13.0](https://github.com/square/okio/tree/okio-parent-1.13.0) です。Android アプリのプロジェクトに導入する際の手順を記載します。

## 依存の追加
[OkHttp](https://github.com/square/okhttp/) や [Moshi](https://github.com/square/moshi) をすでに導入している場合は依存でインストールされています。そうでない場合や明示的にバージョンを指定したい場合は、下記の通り build.gradle に追記してください。

```groovy:app/build.gradle
dependencies {
  implementation 'com.squareup.okio:okio:1.13.0'
```

## ProGuard
公式リポジトリの README に書かれているように、ProGuard を用いる場合は設定の追加が必要です。

```:app/proguard-rules.pro
-dontwarn okio.**
```

----
# 使ってみる
Okio には主に buffer/sink/source と、3つの static メソッドが用意されており、buffer に sink か source を組み合わせて使う、と覚えておくと良いと思います。

## ファイル読み込み
buffer メソッドと source メソッドを使います。source メソッドの引数には java.io.File, java.io.OutputStream, java.net.Socket, java.nio.file.Path が指定できます。

```kotlin:ファイル読み込み
Okio.buffer(Okio.source(file)).readUtf8()
```

文字コードは UTF-8 のみ対応しているようです。これは Android が UTF-8 を標準として用いているからだと思われます。

ちゃんとリソースをクローズするときは下記のように書きます。

```kotlin:リソースをクローズ
val bufferedSource: BufferedSource = Okio.buffer(Okio.source(file))
val text: String? = bufferedSource.readUtf8()
bufferedSource.close()
```

Kotlin の場合はスコープ関数の run を使えば、もう少し冗長さを軽減した書き方ができます。

```kotlin:runを使う
val text: String? = Okio.buffer(Okio.source(file)).let { 
    val text = it.readUtf8()
    it.close()
    return@let text
}
```

## ファイル書き込み
buffer メソッドと sink メソッドを使います。sink メソッドの引数には java.io.File, java.io.OutputStream, java.net.Socket, java.nio.file.Path が指定できます。

sample.json というファイルに出力する場合は下記の通りで書けます。 write メソッドの引数は文字列ではなく、 byte 配列の形式で渡す必要があります。
Kotlin だとこの辺が標準ライブラリの拡張のおかげで多少書きやすくなっています。

```kotlin:ファイル書き込み
Okio.buffer(Okio.sink(File(itemsDir, "sample.json")))
    .write(text.toByteArray(charset("UTF-8")))
    .flush()
```

flush() を呼び忘れると実際のファイルへの書き込みが実施されないことに注意してください。

```kotlin:リソースをクローズ
val bufferedSink: BufferedSink = Okio.buffer(Okio.sink(File(itemsDir, "sample.json")))
bufferedSink.write(text.toByteArray(charset("UTF-8")))
bufferedSink.flush()
bufferedSink.close()
```

これもスコープ関数の run を使って書くことが可能です。

```kotlin:runを使う
Okio.buffer(Okio.sink(file)).run {
    write(contentBytes())
    flush()
    close()
}
```

----
# 応用

## テスト用のリソース読み込み
例えば、テスト用に用いる静的ファイルを ```app/src/test/resources/html/sample.html``` に置いている場合、下記の通りに読み込むことが可能です。

```kotlin:テスト用リソースの読み込み
private fun readSource(): String? {
    val bufferedSource: BufferedSource = Okio.buffer(Okio.source(classLoader.getResourceAsStream("html/sample.html")))
    val sourceText: String? = bufferedSource.readUtf8()
    bufferedSource.close()
    return sourceText
}
```

let を用いる場合は下記の通りです。

```kotlin:letを使う
private fun readSource(): String? =
            Okio.buffer(Okio.source(classLoader.getResourceAsStream("html/sample.html"))).let {
                val sourceText: String? = it.readUtf8()
                it.close()
                return sourceText
            }
```

## Storage Access Framework で指定したファイルに対して書き込みを実行
RuntimePermission の処理は割愛します。

```kotlin:Intent&nbsp;初期化と&nbsp;Storage&nbsp;Access&nbsp;Framework&nbsp;の呼び出し
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "text/html"
intent.putExtra(Intent.EXTRA_LOCAL_ONLY, true)

startActivityForResult(intent, REQUEST_CODE_IMPORT_BOOKMARK)
```

```kotlin:Storage&nbsp;Access&nbsp;Framework&nbsp;の結果を受け取ってメソッドを呼び出す
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
    if (intent == null || resultCode != Activity.RESULT_OK) {
        return
    }

    when (requestCode) {
        REQUEST_CODE_EXPORT_BOOKMARK -> writeToContentUri(intent.data)
    }
}
```

```kotlin:Uri&nbsp;で指定されたファイルへの書き込みを実行
Okio.buffer(Okio.sink(contentResolver.openOutputStream(uri))).run {
    .writeUtf8(Exporter(bookmarks).invoke())
    .flush()
```

run を使う場合は以下の通りです。

```kotlin:runを使ってUri&nbsp;で指定されたファイルへの書き込みを実行
Okio.buffer(Okio.sink(contentResolver.openOutputStream(uri))).run {
    writeUtf8(Exporter(bookmarks).invoke())
    flush()
    close()
}
```

----

# まとめ
Android アプリ開発の際に役立つファイル操作用ライブラリの Okio について説明しました。Android SDK での開発だと煩雑になりそうなファイル入出力を楽に実施できるライブラリです。Kotlin のスコープ関数と組み合わせるとより簡潔な記述も可能になります。HTTP クライアントに OkHttp を使っている場合は依存で入っていますので、せっかくですから使ってみても良いかもしれません。

## 参考
- [okio (Okio 1.13.0 API) - Square Open Source](https://square.github.io/okio/1.x/okio/)
- [A Few 'Ok' Libraries (Droidcon MTL 2015)](https://speakerdeck.com/jakewharton/a-few-ok-libraries-droidcon-mtl-2015)
- [OkioTest.java](https://github.com/square/okio/blob/okio-parent-1.13.0/okio/src/test/java/okio/OkioTest.java)
- [Mastering Kotlin standard functions: run, with, let, also and apply](https://android.jlelse.eu/mastering-kotlin-standard-functions-run-with-let-also-and-apply-9cd334b0ef84)