【Kotlin】WebViewのキャッシュに対応する

謎のプリン語る。
プログラミングの役立つ、エンジニア技術情報とか、どうでもいい雑談とか書いてます。
一人書く人増えました。

【Kotlin】WebViewのキャッシュに対応する

【Kotlin】WebViewのキャッシュに対応する - サムネイル

またまた一年以上空いてしまった。
生きてました。
最近は、KotlinでのAndroidアプリもやってたりします。
Unityも手出し始めてて、もう何屋さんかわからなくなってる。
Unityの記事もそのうち書きたい。

さて、さっそく表記の話。
どういうことかというと、Kotlin(というかAndroid)のWebViewはキャッシュ関連のコントロールが少しめんどいので、その対応についてである。
早い話が、キャッシュがクリアできない問題だ。

KotlinのWebViewのキャッシュは基本強い。
デフォルトの設定では、キャッシュが存在する場合は間違いなくキャッシュを使用する。
サーバー側から新しい状態をレスポンスしてもお構い無しだ。
ただし、通常は、キャッシュを使わない設定をすれば解決する。

import android.webkit.WebSettings

// 中略

webView.settings.cacheMode = WebSettings.LOAD_NO_CHACHE
// webViewには、WebViewクラスのインスタンス等が格納されているとする

見た感じから分かる通り、キャッシュ使いません!設定をするのだが、
厄介なことに、AOSやデバイスによって、この設定が効かないパターンが存在する。

私自身、この、効かないパターンにより、デバイス別で現象が変わるという厄介な状況に追い込まれ、調査等を行っていった。
そして、ある一定の効果がある方法を編み出した。

まず一つは、事前にhttpリクエストでキャッシュを介さないアクセスをし、ファイル更新日で比較する手法だ。
下記のようになる。
(以下ソースコードは、WebViewを拡張したクラスでのソースコードとなる)

package プロジェクトパッケージ

import android.content.Context
import android.content.SharedPreferences
import android.util.AttributeSet
import android.util.Log
import android.webkit.WebView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

import java.net.HttpURLConnection
import java.net.URL

import java.util.Calendar
import java.util.Locale

class WebViewExtends : WebView {
    private var _cacheKey: String? = null

    // 通常のWebViewと同じコンストラクタ群
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int)
            : super(context, attrs, defStyleAttr, defStyleRes)


    constructor(context: Context, cacheKey: String) : super(context) {
        this._cacheKey = cacheKey
    }

    fun setCacheKey(key: String = ""){
        this._cacheKey = key
    }


    private fun fetchLastModified(urlString: String, onResult: (String?) -> Unit) {
        CoroutineScope(Dispatchers.IO).launch {
            var connection: HttpURLConnection? = null
            try {
                val url = URL(urlString)
                connection = url.openConnection() as HttpURLConnection
                connection.requestMethod = "HEAD"
                val lastModified = connection.getHeaderField("Last-Modified")
                // 結果をメインスレッドに返す場合
                kotlinx.coroutines.withContext(Dispatchers.Main) {
                    onResult(lastModified)
                }
            } catch (e: Exception) {
                e.printStackTrace()
                kotlinx.coroutines.withContext(Dispatchers.Main) {
                    onResult(null)
                }
            } finally {
                connection?.disconnect()
            }
        }
    }

    override fun loadUrl(url: String) {
        fetchLastModified(url) { lastModified ->
            // アプリ保持更新日と比較し、新しければアプリ内キャッシュクリア
            if (lastModified != null) {
                // 値を利用
                val sharedPref: SharedPreferences = context.getSharedPreferences(
                    "cache_pref", // ファイル名(任意の文字列でOK)
                    Context.MODE_PRIVATE
                )
                val stringValue = sharedPref.getString(this._cacheKey, "default modi")

                if (stringValue != lastModified) {
                    this.clearCache(true)
                    sharedPref.edit().putString(this._cacheKey, lastModified)
                }

            }

            // 必ず親クラスのloadUrlを呼び出す
            super.loadUrl(url)
        }

    }
}

キャッシュを介さないアクセスを事前にし、ファイル更新日によって、キャッシュをクリアするか、しないかをコントロールする手法だ。
比較的スマートにできていると思う。
ただし、弱点としては、アクセスが重複している点だ。

よって、下記の方法が望ましいように思う。

// 基本のインポート文定義は省略

class WebViewExtends : WebView {
    private var _cacheKey: String? = null

    // コンストラクターも省略

    fun setCacheKey(key: String = ""){
        this._cacheKey = key
    }

    // 10分区切りで丸めた日付文字列を付与
    private fun appendTimestamp(url: String): String {
        val ts = getRoundedTimestamp()
        val separator = if (url.contains("?")) "&" else "?"
        return "$url${separator}ts=$ts"
    }

    // 10分単位で丸めた日付文字列(例: 202507141750)を生成
    private fun getRoundedTimestamp(): String {
        val cal = Calendar.getInstance()
        val year = cal.get(Calendar.YEAR)
        val month = cal.get(Calendar.MONTH) + 1 // Calendar.MONTHは0始まり
        val day = cal.get(Calendar.DAY_OF_MONTH)
        val hour = cal.get(Calendar.HOUR_OF_DAY)
        val minute = (cal.get(Calendar.MINUTE) / 10) * 10

        // ゼロ埋めしてフォーマット
        return String.format(Locale.US, "%04d%02d%02d%02d%02d", year, month, day, hour, minute)
    }

    override fun loadUrl(url: String) {
        val setUrl = appendTimestamp(url)
        super.loadUrl(setUrl)
    }

}

要は、アクセスURLに対して、タイムスタンプパラメータを付与するというものだ。
(本記事では、10分おきのタイムスタンプとしている)
さしものKotlinのWebViewといえど、さすがにURLパラメータが違えば、新規アクセスとなる。
ただし、こちらの手法も弱点があり、キャッシュが使用される時間が決まった時間内のみとなる点だ。

だが、以上の手法であれば、キャッシュを使わない設定が効かないパターンでも有効だ。

KotlinというかAndroidは面白いね。
アプリ作る際も、Swift(Xcode)での開発よりはるかに自由度が高く作れるから、いろいろできそう。
ただ、その分、全部自分で作らなきゃいけない感があって、ハードルが高い印象はある・・・。

また、いろいろ記事書いていこうと思うよ。
Unityのノウハウ記事も書きたい。

ではまた。

格安でソーシャルゲームイラストを構築します【EAGLEGRAPHICS】 - メイン

著者

みやびプリン 職業:フロントエンドエンジニア
基本はイラストレイター(自称)だが、
本職は札幌市のフロントエンドエンジニア。
フロントエンドだけではなく、各種プログラミング言語に精通していると自負している。
HTML、CSSはもちろんのこと、JavaScript、TypeScript、PHP、Perl、Python、C#、Kotlin、Swift、Objective-Cなど多くの多くの言語に精通している他、
Movable Type、WordPressなど各種CMS、React.js、Angularなどフロントエンドフレームワークも扱える。
最近はUnityによる3Dコンテンツにも手を出している。

トラックバック(0)

トラックバックURL:

コメントする

ページトップへ戻る