2022年2月23日水曜日

StateFlow.collectAsState() だと現在の値を初期値として使ってくれる

StateFlow.collectAsState() だと StateFlow の value を初期値として使ってくれます。一方 Flow.collectAsState() では initial に指定した値が初期値になります。 @Composable fun <T> StateFlow<T>.collectAsState( context: CoroutineContext = EmptyCoroutineContext ): State<T> = collectAsState(value, context) @Composable fun <T : R, R> Flow<T>.collectAsState( initial: R, context: CoroutineContext = EmptyCoroutineContext ): State<R> = produceState(initial, this, context) { if (context == EmptyCoroutineContext) { collect { value = it } } else withContext(context) { collect { value = it } } }

例えばサーバーからデータをとってきて表示する画面があり、画面の状態を表す UiState が次のようになっているとします。 sealed interface UiState { object Initial : UiState object Loading : UiState data class Error(val e: Exception) : UiState data class Success(val profile: Profile) : UiState }
(* 私は基本的には ViewModel からは StateFlow ではなく State を公開するようにしています。)


Flow.collectAsState() の場合 class ProfileViewModel : ViewModel() { val uiState: Flow<UiState> = ... ... } @Composable fun ProfileScreen( viewModel: ProfileViewModel ) { ProfileContent( uiState = viewModel.uiState.collectAsState(initial = UiState.Initial).value ) } @Composable private fun ProfileContent( uiState: UiState ) { when (uiState) { UiState.Initial, UiState.Loading -> { ... } is UiState.Error -> { ... } is UiState.Success -> { ... } } } Flow.collectAsState() の場合、ProfileContent の (re)compose およびそのときの UiState は次のようになります。

UiState.Initial → (ViewModel でデータ取得開始)→ UiState.Loading → (取得成功)→ UiState.Success

ここで ProfileScreen から Navigation Compose で別の画面に行って戻ってくると、一瞬 UiState.Initial になります。

UiState.Success → (Navigation Compose で別の画面に行って戻ってくる) → UiState.Initial → UiState.Success



StateFlow.collectAsState() の場合 class ProfileViewModel : ViewModel() { val uiState: StateFlow<UiState> = ... ... } @Composable fun ProfileScreen( viewModel: ProfileViewModel ) { ProfileContent( uiState = viewModel.uiState.collectAsState().value ) } StateFlow.collectAsState() の場合、データ取得完了までの UiState の流れは同じです。

UiState.Initial → (ViewModel でデータ取得開始)→ UiState.Loading → (取得成功)→ UiState.Success

一方、ここで ProfileScreen から Navigation Compose で別の画面に行って戻ってきても UiState.Initial にはなりません。

UiState.Success → (Navigation Compose で別の画面に行って戻ってくる) → UiState.Success

そのため、ViewModel から StateFlow を公開して StateFlow の collectAsState() を使ったほうがよいです。
stateIn() を使えば Flow を StateFlow に変換することができます。