①init内で発火するCoroutineでasyncして、他のsuspend関数でawaitする場合の実装ミス
タイミング問題で表面化しない可能性があるので厄介です。
initはsuspend関数ではないため、coroutineを起動して時間のかかるフェッチ動作等を非同期的に実行させる必要があります。そのフェッチ結果が必要なsuspend関数ではフェッチを終えるまでawaitします。
例えば次のように実装したとします。
private lateinit var something: Defered<Something> init{ externalScope.launch{ something = async { fetchSomething() } } } suspend fun getSomething(): Something { return something.await() //NG }
一見よさそうですが、これは失敗する可能性があります。
launch()
↓
init終了
↓
getSomething()
と来た場合、”lateinit property xx has not been initialized
“Errorが発生します。coroutineをlaunchしてもasync{}がいつ実行されるかはスレッド処理に委ねられるため、場合によってはasync{}によってDeferedが渡される前にアクセスされてしまう可能性があります。
したがって、解決策としては以下になります。
①jobで完了するまで待つ
使用箇所が多くない場合に有効です。Job.join()を利用して、coroutineが完了するまでsuspend関数をwaitさせます。ただし、使用箇所が増えるとあらゆる箇所でJob.joinをわざわざ記載する必要があり、boilerplateなコードが増えてしまいます。ただfieldをlateinitにできるし、初期値も与える必要がないので一番忠実で分かりやすいコードとも言えます。
private lateinit var something: Defered<Something> private val job: Job init{ job = externalScope.launch{ something = async { fetchSomething() } } } suspend fun getSomething(): Something { job.join() return something.await() //NG }
②初期値を与える
意味のある初期値を定義できる場合には初期値を定義してしまう方法が簡便です。suspend関数でありながらベストエフォート型の動作となります。(fetchSomething()が完了していた場合には意味のあるデータを返し、完了していない場合にはNOT_INITIALIZEDを返します。)
この場合、NOT_INITIALIZEDでの状態動作をApplication側で決定することが可能です。①ではwaitするだけだったのに対して、柔軟に表示を切り替えることが可能です。
private var something: Something = Something.NOT_INITIALIZED private val job: Job init{ job = externalScope.launch{ something = fetchSomething() } } suspend fun getSomething(): Something { return something }
③Flowの利用
②は結局Application側が値を2回取りに行く必要があるので、Application側の実装コストが高まります。KotlinのFlowは値が一連の状態変化を行う場合にシンプルにコードを記載できるように導入されていますので、これを利用するのが良いのかもしれません。
private var something = MutableSharedFlow<Something>(replay=1) private val job: Job init{ job = externalScope.launch{ something.emit(fetchSomething()) } } suspend fun getSomething(): MutableSharedFlow<Something> { return something }
SharedFlow(replay=1)と実装することで、Application側でSubscribe(collect)した場合に新しい値を取得することが可能です。また、bufferされていない場合は何もemitしません。