【Android】アプリ内キャッシュで見た目のパフォーマンスとUXを改善する

はじめに

スマホアプリにおいて、動作やレスポンスが遅いことは重大な問題です。アプリのローディングが長いと、それだけでユーザは離脱してしまいます。
アプリ内キャッシュは、ネットワークが悪い状況などでもレスポンスの遅さを解決する有力な選択肢となります。

アプリ内キャッシュのメリット・デメリット

アプリ内キャッシュの導入はUXを向上させる一方で、開発におけるトレードオフも存在します。導入を決める前に、そのメリットとデメリットを正しく理解しておきましょう。

メリット:UXの向上とリソースの節約

  • 見た目のパフォーマンス向上によるUX改善
    • アプリを開いた瞬間にデータが表示されるため、ユーザーに「待たされている感覚」を与えません。これによりUXが大きく向上します。
  • 通信量とバッテリーの節約
    • 同じデータを何度も再ダウンロードする無駄を省き、通信量やバッテリーの消費も改善されます。
  • 劣悪なネットワーク環境への耐性
    • 地下鉄などの電波が極端に悪い場所でもアプリの機能や画面表示を維持できます。通信エラーでユーザーの操作を妨げることを予防できます。


デリット:OSによる削除リスクとデータ鮮度の管理

  • OSによる「勝手なキャッシュ削除」への考慮
    • Android OSは、デバイスのストレージ容量が逼迫するとアプリの都合とは関係なくキャッシュ領域のデータを自動的に削除します。そのため、認証情報やユーザーが作成した未送信のデータなど、「消えて困るデータ」はキャッシュ領域で扱ってはなりません。基本的には「いつでもサーバーから再取得・最新化できるデータ」に用途が限定されます。
  • データの「先祖返り」や反映遅延のリスク
    • キャッシュを表示するということは、一時的に「古いデータ」をユーザーに見せることになります。適切に更新を行わないと、画面の情報が古いままになるリスクがあります。
  • 処理フローが複雑になる
    • 通信して表示するだけの実装から、「キャッシュを探す ➔ 表示する ➔ 通信で最新化する ➔ 差分を更新する」といった非同期かつ複雑なデータフロー(並行処理や排他制御など)を設計・実装・テストする必要が生じます。

アプリ内キャッシュの使い所と実装例

キャッシュ利用の効果が最も高いのはやはり画像や動画などのダウンロードと言えますが、それらはライブラリが既にキャッシュサポートをしているのが一般的で、意識せずとも最適化されていることがほとんどと思われます。
一方で、APIリクエストのレスポンスは、独自でキャッシュ制御する費用対効果が高いと言えるでしょう。
単純にAPIレスポンスをキャッシュするだけならAPIリクエストのライブラリがサポートするキャッシュ制御で十分な場合もあるでしょうが、Androidのアプリ内キャッシュを理解して取り扱えば、例えば以下のような制御が可能となります。
「最新の状態が比較的頻繁に変わるAPIリクエストについて、キャッシュによる先行表示をしつつAPIリクエストも実際に行って最新の状態を維持する」

以下に、独自でキャッシュ制御する実装例を示します。

companion object {
    const val SOME_DATA_CACHE_NAME = "some_cache_file"
}

suspend fun loadData() {
    loadFromCache(context)?.let { cacheData ->
        // cacheDataを使って画面表示処理
    }

    loadFromServer()?.let { latestData ->

        // latestDataを使って画面表示処理

        saveToCache(context, latestData)
    }
}

suspend fun loadFromCache(context: Context): SomeData? = withContext(Dispatchers.IO) {
    val cacheFile = File(context.cacheDir, SOME_DATA_CACHE_NAME)

    if (!cacheFile.exists()) return@withContext null

    cacheFile.bufferedReader().use { reader ->
        Gson().fromJson(reader, SomeData::class.java)
    }
}

suspend fun loadFromServer(): SomeData?
{
    // アプリ独自のデータをサーバーから取得
}

suspend fun saveToCache(context: Context, latestData: SomeData)
{
    withContext(Dispatchers.IO) {
        val file = File(context.cacheDir, SOME_DATA_CACHE_NAME)
        file.bufferedWriter().use { writer ->
            Gson().toJson(latestData, writer)
        }
    }
}

これにより、画面表示はキャッシュを利用して最速で表示しつつ、サーバーへのリクエストも行ってデータの最新化も両立することができます。

スプレッドワンではAndroidアプリの開発案件も承っています。お気軽にお問い合わせ下さい。