2022年2月23日水曜日

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

StateFlow.collectAsState() だと StateFlow の value を初期値として使ってくれます。一方 Flow.collectAsState() では initial に指定した値が初期値になります。
  1. @Composable  
  2. fun <T> StateFlow<T>.collectAsState(  
  3.     context: CoroutineContext = EmptyCoroutineContext  
  4. ): State<T> = collectAsState(value, context)  
  5.   
  6. @Composable  
  7. fun <T : R, R> Flow<T>.collectAsState(  
  8.     initial: R,  
  9.     context: CoroutineContext = EmptyCoroutineContext  
  10. ): State<R> = produceState(initial, this, context) {  
  11.     if (context == EmptyCoroutineContext) {  
  12.         collect { value = it }  
  13.     } else withContext(context) {  
  14.         collect { value = it }  
  15.     }  
  16. }  


例えばサーバーからデータをとってきて表示する画面があり、画面の状態を表す UiState が次のようになっているとします。
  1. sealed interface UiState {  
  2.     object Initial : UiState  
  3.     object Loading : UiState  
  4.     data class Error(val e: Exception) : UiState  
  5.     data class Success(val profile: Profile) : UiState  
  6. }  

(* 私は基本的には ViewModel からは StateFlow ではなく State を公開するようにしています。)


Flow.collectAsState() の場合
  1. class ProfileViewModel : ViewModel() {  
  2.   
  3.     val uiState: Flow<UiState> = ...  
  4.   
  5.     ...  
  6. }  
  1. @Composable  
  2. fun ProfileScreen(  
  3.     viewModel: ProfileViewModel  
  4. ) {  
  5.     ProfileContent(  
  6.         uiState = viewModel.uiState.collectAsState(initial = UiState.Initial).value  
  7.     )  
  8. }  
  9.   
  10. @Composable  
  11. private fun ProfileContent(  
  12.     uiState: UiState  
  13. ) {  
  14.     when (uiState) {  
  15.         UiState.Initial,  
  16.         UiState.Loading -> {  
  17.             ...  
  18.         }  
  19.         is UiState.Error -> {  
  20.             ...  
  21.         }  
  22.         is UiState.Success -> {  
  23.             ...  
  24.         }  
  25.     }  
  26. }  
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() の場合
  1. class ProfileViewModel : ViewModel() {  
  2.   
  3.     val uiState: StateFlow<UiState> = ...  
  4.   
  5.     ...  
  6. }  
  1. @Composable  
  2. fun ProfileScreen(  
  3.     viewModel: ProfileViewModel  
  4. ) {  
  5.     ProfileContent(  
  6.         uiState = viewModel.uiState.collectAsState().value  
  7.     )  
  8. }  
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 に変換することができます。