パス設定の読み込み状態を StateFlow で観測可能に
PathConfiguration に StateFlow ベースの読み込み状態管理が導入され、アプリはパス設定がいつ・どのソースから読み込まれたかを観測できるようになりました。新たに PathConfigurationLoadState と PathConfigurationData が追加され、状態の型安全な表現と軽量なデータ転送を実現しています。
背景
これまでの PathConfiguration は読み込み完了を通知する手段を持たず、設定がいつ利用可能になるかをアプリ側から知る方法がありませんでした。バンドルアセット・キャッシュ済みリモート・フレッシュリモートという3つのソースを持ちながら、どのソースから読み込まれたかの区別もできませんでした。
この制約は、設定の読み込み完了後に初期化処理を走らせたいケースや、キャッシュとリモートで異なる挙動を実装したいケースで問題となっていました。本PRはこれらのユースケースに対し、Kotlin の StateFlow を中心とした観測可能な状態管理の仕組みを導入することで対応しています。
技術的な変更
PathConfigurationLoadState — 型安全な状態の表現
PathConfigurationLoadState として、読み込み状態を型安全に表現する sealed interface が新設されました。状態は NotLoaded(初期状態)と Loaded(読み込み完了)の2系統に分かれ、Loaded はさらに3つのサブクラスに分岐します。
sealed interface PathConfigurationLoadState {
data object NotLoaded : PathConfigurationLoadState
sealed interface Loaded : PathConfigurationLoadState {
val configuration: PathConfigurationData
data class BundledAssetLoaded(override val configuration: PathConfigurationData) : Loaded
data class CachedRemoteLoaded(override val configuration: PathConfigurationData) : Loaded
data class RemoteLoaded(override val configuration: PathConfigurationData) : Loaded
}
}
is Loaded で読み込み完了を一括チェックし、ソースを区別する必要がある場合のみ具体的なサブクラスに対して when 分岐するという2段階の判定が可能になっています。
PathConfigurationData — シリアライズ可能な軽量データクラス
PathConfigurationData は、PathConfiguration から設定データ(rules・settings)を分離した data class として新設されました。もともと PathConfiguration 自体に @SerializedName アノテーションが付いていた rules と settings フィールドを抽出し、Gson によるデシリアライズ対象をこのクラスに集約しています。
data class として定義されていることで equals()/hashCode() が自動生成され、StateFlow が同一内容の連続エミットを重複排除できます。PathConfigurationData は properties(location: String) メソッドも備えており、URLに対するパス設定の検索ロジックが PathConfiguration から移されています。
PathConfiguration — StateFlow の公開と同期・非同期の分離
PathConfiguration に loadState: StateFlow<PathConfigurationLoadState> が追加され、内部では MutableStateFlow として管理されます。
class PathConfiguration internal constructor() {
private val _loadState = MutableStateFlow<PathConfigurationLoadState>(NotLoaded)
private val loadingScope: CoroutineScope = CoroutineScope(dispatcherProvider.io + SupervisorJob())
private var loadingJob: Job? = null
val loadState: StateFlow<PathConfigurationLoadState> = _loadState.asStateFlow()
}
バンドルアセットとキャッシュ済みリモートの読み込みは load() 内で同期的に実行され、load() が返った時点で設定が即座に利用可能になります。リモートフェッチは Dispatchers.IO 上で非同期に実行されます。load() が複数回呼ばれた場合は前回の loadingJob がキャンセルされ、多重フェッチを防ぎます。状態の変更は synchronized ブロックで保護され、IOスレッドとメインスレッド間のスレッド安全性が確保されています。
PathConfigurationLoader — 読み込み順序の変更と責務の整理
PathConfigurationLoader はコルーチンスコープを自身で持つ構造から、ロード結果を戻り値として返す純粋な関数群に変わりました。コンストラクタから Context が除かれ、ロード関数の引数として受け取る設計に変更されています。
-internal class PathConfigurationLoader(val context: Context) : CoroutineScope {
+internal class PathConfigurationLoader {
読み込み順序にも変更があります。従来はバンドルアセットを先に読み込んでいましたが、今回からはキャッシュ済みリモート設定を優先します。キャッシュが存在しない場合、またはキャッシュのパースに失敗した場合にのみバンドルアセットにフォールバックします。これにより、以前リモートから取得した設定がある場合は初回起動時からそれが優先されます。
PathConfigurationRepository — OkHttp 5 の非同期 API に移行
依存ライブラリが OkHttp 4.12.0 から 5.3.2 に更新され、新たに okhttp-coroutines アーティファクトが追加されました。issueRequest は suspend 関数になり、call.execute() から call.executeAsync() へ変更されています。
- private fun issueRequest(request: Request): String? {
- return try {
- val call = HotwireHttpClient.instance.newCall(request)
- call.execute().use { response ->
+ private suspend fun issueRequest(request: Request): String? = try {
+ val call = HotwireHttpClient.instance.newCall(request)
+ call.executeAsync().use { response ->
+ withContext(dispatcherProvider.io) {
また response.body?.string() から response.body.string() へのnon-null化も行われており、OkHttp 5 での API 変更に追従しています。
DemoApplication — 初期化順序の修正
DemoApplication では Hotwire.loadPathConfiguration() の呼び出しが configureApp() の先頭から末尾に移動されました。これはパス設定の読み込み前に他の設定(JSONコンバーターなど)が確定している必要があるケースへの対応と見られます。
設計判断
StateFlow を採用したリアクティブな状態公開が本PRの中心的な設計判断です。コールバックによる完了通知ではなく StateFlow を選ぶことで、collect・first・filter といった Flow 演算子をそのまま活用でき、「最初の読み込み完了を一度だけ待つ」「特定ソースの読み込みだけを監視する」といった多様なユースケースに対応できます。
後方互換性への配慮も随所に見られます。バンドルアセットの読み込みを同期的に維持することで、load() 後に即座に設定を参照する既存コードが壊れません。PathConfigurationData を data class とし equals()/hashCode() を自動生成させることで、同一内容での再読み込みが StateFlow の不要な再エミットを引き起こさない設計になっています。
sealed interface による状態の階層化も注目すべき点です。Loaded を中間の sealed interface として設け、その下に具体的なソースを表すサブクラスを置く2層構造により、「読み込み完了かどうか」と「どこから読み込まれたか」を独立して扱えます。
まとめ
本PRは、パス設定の読み込みをブラックボックスから観測可能なプロセスへと変換した変更です。PathConfigurationLoadState の sealed 階層と StateFlow の組み合わせにより、バンドル・キャッシュ・リモートという3段階の読み込みシーケンスをアプリ側が細かく制御できるようになり、設定の可用性に依存した初期化ロジックの実装が型安全かつシンプルに記述できます。