Android Kotlin 怖くないAsyncTaskLoader


タイトルは釣りです。すいません。

AsyncTaskLoaderを気軽に使えるように整理したのでメモとして残しておきます。

基本形

  • 別スレッドでの処理はAsyncTaskLoaderのloadInBackgroundに記述
  • loadInBackgroundで完了した処理データをUIスレッドで操作するにはLoaderManager.LoaderCallbacksのonLoadFinishedに記述
supportLoaderManager
  1. supportLoaderManagerinitLoader()を実行してLoader(別スレッド)が起動。
  2. supportLoaderManagerはActivity内で一つしか存在しない
    • Activity内で複数のLoaderを扱いたい時はinitLoader()の第一引数idを変更して管理します。
    • 例えば、supportLoaderManager.initLoader(1, null, callbacksA)とした後、supportLoaderManager.initLoader(1, null, callbacksB)としてもcallbacksAに定義されたローダーが再び呼び出される。正しくはidを変更してsupportLoaderManager.initLoader(2, null, callbacksB)とします。
LoaderManagerのコールバック
  1. initLoader()にはLoaderManagerがコールバックするLoaderの生成処理Loaderがロードを完了した時の処理Loaderがリセットされた時の処理のインターフェース実装を渡します。
    • initLoader()時にLoaderの生成処理がコールバックされます。
  2. Loaderの生成処理はAsyncTaskLoaderを実装したオブジェクトを生成してLoaderManagerに返します。
AsyncTaskLoaderの実装
  1. AsyncTaskLoaderは実際にバックグラウンド(別スレッド)で行う処理バックグランドで処理を開始する準備が整った時に呼び出される処理(onStartLoading)を記述します。
    • 理由はよくわからないのですが、私の環境ではinitLoader()後、ActivityのonResumeがトリガされたタイミングでinitLoaderせずともonStartLoadingが呼び出されるような挙動を確認しました。必要に応じて、正しいAsyncTaskLoaderの使い方 - Qiita この記事のように再ロード防止のフラグを設けます。

上記の実装の最小構成が以下となります。

val context = this
//supportLoaderManagerがコールバックするメソッドの実装
val callbacks = object : LoaderManager.LoaderCallbacks<String> {
    //ローダー生成の実装
    override fun onCreateLoader(id: Int, args: Bundle?): Loader<String> {
        return object : AsyncTaskLoader<String>(context) {
            //生成したローダーが行う処理の実装
            override fun loadInBackground(): String? {
                return null
                }
            }
            override fun onStartLoading() {
                forceLoad()
            }
        }
    }

    override fun onLoadFinished(loader: Loader<String>, data: String?) {
    }

    override fun onLoaderReset(loader: Loader<String>) {
    }
}
//ローダーの起動
supportLoaderManager.initLoader( 1, null, callbacks )

ジェネリクスはloadInBackgroundで返却されるオブジェクトの型です。大半の場合はStringで良いかと思います。

インターフェース実装をメソッド化して取り扱う

他の各Activityや同じActivity内で複数のローダーが必要な場合など実装を生成しやすいようメソッド化します。 各、コールバックメソッドの実装をエンクロージャ引数として渡すようにすれば、呼び出し側から各処理の中身を定義することができます。

fun <T>createLoaderCallbacks(
    context: Context,
    backgroundTask:(()->T?)?,
    onLoadFinishedTask:((Loader<T>, T?)->Unit)?,
    onLoaderResetTask:((Loader<T>)->Unit)?
    ):LoaderManager.LoaderCallbacks<T>
{
    return object : LoaderManager.LoaderCallbacks<T> {
        override fun onCreateLoader(id: Int, args: Bundle?): Loader<T> {
            return object : AsyncTaskLoader<T>(context) {
                override fun loadInBackground(): T? {
                    return backgroundTask?.let {
                        backgroundTask!!()
                    }
                }
                override fun onStartLoading() {
                    forceLoad()
                }
            }
        }

        override fun onLoadFinished(loader: Loader<T>, data: T?) {
            onLoadFinishedTask?.let {
                onLoadFinishedTask!!(loader, data)
            }
        }

        override fun onLoaderReset(loader: Loader<T>) {
            onLoaderResetTask?.let {
                onLoaderResetTask!!(loader)
            }
        }
    }
}

呼び出し側の実装

呼び出し側はシンプルに実装できます。

private fun getWord():String?{
    return "文字列"
}
private fun printWord(loader:Loader<String>, data:String?){
    Log.d("word", data)
}

private fun doBackground() {
    //引数つきの関数オブジェクト
    val funcAssignTopics:(Loader<String>, String?)->Unit = ::assignTopics
    //無名関数の場合
    //val funcAssignTopics:(Loader<String>, String?)->Unit = fun (loader:Loader<String>, data: String?){}
    //ラムダ式で渡す場合
    //val funcAssignTopics:(Loader<String>, String?)->Unit = { loader, s -> }

    val callbacks = createLoaderCallbacks<String>(
        this,
        ::getWord,
        funcAssignTopics,
        // { loader, s -> } ラムダ式で記述も出来る
        null )

    val lm = supportLoaderManager
    lm.initLoader(1, null, callbacks)
}

このように必要な処理のみ記述でコールバックの実装の生成が可能です。

参考 https://developer.android.com/guide/components/loaders?hl=ja