Composeの初期化処理を snapshotFlow と first で待つ

Composeで画面を初期化するとき、次のような問題に遭遇することがあります。


画面が表示される
 ↓ 
Stateを復元する
 ↓ 
非同期データを読み込む
 ↓
UIにデータが反映される
 ↓ 
初期処理を実行する

例えば、保存していたスクロール位置を復元したいとします。

しかし、LaunchedEffect が実行された時点では、まだ Grid にアイテムが存在しないかもしれません。


LaunchedEffect(Unit) { 
    gridState.scrollToItem( 
        savedPosition.index, 
        savedPosition.offset 
    ) 
}

この場合、保存された位置が100番目だったとしても、初期状態のGridにはまだ20個しかアイテムがない可能性があります。


LaunchedEffect
 ↓ 
totalItemsCount = 20
 ↓ 
scrollToItem(100)
 ↓ 
⚠️ まだ100番目のアイテムが存在しない

そこで、snapshotFlowfirst を使います。

 

🧑🏻‍💻 snapshotFlow { }.first { }というパターン


snapshotFlow { 
    composeState 
}.first { 
    condition 
}

これは、次のような処理です。


composeState を監視
 ↓ 
State が変化する
 ↓ 
条件を確認
 ↓ 
condition を満たす
 ↓
次の処理へ進む

first は、条件を満たす最初の値を受け取るとFlowの収集を終了します。

そのため、

UIの状態が初期処理に必要な状態になるまで待つ

のような用途に向いています。

 

🧑🏻‍💻 スクロール位置を復元する

次のように書けます。


LaunchedEffect(Unit) { 
    val savedPosition = viewModel.savedScrollPosition 

    snapshotFlow {
        gridState.layoutInfo.totalItemsCount 
    }.first { 
        it > savedPosition.index 
    } 

    gridState.scrollToItem( 
        savedPosition.index, 
        savedPosition.offset 
    ) 
}

ここで重要なのは、次の2つの状態は同じではないことです。


データが存在する
  ≠
UIのLayoutにアイテムが反映されている

ViewModel がデータを保持していても、Compose の Layout がまだ計算されていないことがあります。

このコードでは、


gridState.layoutInfo.totalItemsCount

を監視しています。

そして、


.first { 
    it > savedPosition.index 
}

によって、保存された位置までアイテムがGridに反映されるのを待ちます。

例えば、保存位置が100の場合は次のようになります。


totalItemsCount = 0
 ↓ 
totalItemsCount = 50
 ↓ 
totalItemsCount = 100
 ↓ 
totalItemsCount = 120
 ↓ 
条件成立
 ↓ 
scrollToItem(100)

このように、scrollToItem() を実行するタイミングを、単なる画面表示時ではなく、UIが実際に必要な状態になったタイミングに合わせることができます。

 

🧑🏻‍💻 なぜ collect ではないのか

snapshotFlow は Flow なので、通常は次のように collect できます。


snapshotFlow {
    gridState.layoutInfo.totalItemsCount
}.collect { count ->
    // countが変化するたびに実行
}

しかし、初期化処理では、継続的に監視したいわけではありません。


必要な状態になるまで待つ
 ↓
初期処理を実行する
 ↓ 
監視を終了する

この場合は、


.first { condition }

が適しています。


snapshotFlow { 
    gridState.layoutInfo.totalItemsCount 
}.first { 
    it > savedPosition.index 
} 

gridState.scrollToItem(...)

first が条件を満たすと、次の処理に進みます。


snapshotFlow
  ↓
first
  ↓
条件成立
  ↓
Flow終了
  ↓
scrollToItem()

 

🧑🏻‍💻 collect と first の使い分け

例えば、スクロール位置を継続的に監視する場合は、collect が自然です。


LaunchedEffect(listState) { 
    snapshotFlow { 
        listState.firstVisibleItemIndex 
    }.collect { index -> 
        // スクロールするたびに処理 
    } 
}

一方、初期状態が揃うまで待つ場合は、first が自然です。


LaunchedEffect(Unit) {
    snapshotFlow {
        gridState.layoutInfo.totalItemsCount
    }.first {
        it > savedPosition.index
    }

    // 初期処理
}

 

🧑🏻‍💻 まとめ

snapshotFlow { }.first { } は、次のようなコードとして理解すると分かりやすいです。


snapshotFlow {
    UIの状態
}.first {
    初期処理に必要な状態になった
}

初期処理()

つまり、

Compose UIの状態が揃うまで待ってから、初期処理を実行する

ためのパターンです。

特に、非同期データの読み込みと Compose の Layout 計算のタイミングがずれるケースでは、非常に有効です。

LazyGridState.layoutInfo のようなLayout情報は、再レイアウトやスクロールによって更新される Observable な状態であり、Android公式ドキュメントでも副作用で扱う場合は snapshotFlow を使う方法が示されています。


Related Categories :  AndroidJetpackComposeKotlinKotlin Multiplatform MobileTrending