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 に変換することができます。


2021年12月16日木曜日

Dagger Hilt 2.40.2 で EntryPoints.get() の便利 overloads である EntryPointAccessors が追加された

今まで val entryPoint = EntryPoints.get(activity, ActivityCreatorEntryPoint::class.java) EntryPointAccessors を使うと val entryPoint = EntryPointAccessors.fromActivity<ActivityCreatorEntryPoint>(activity) fromApplication(), fromActivity(), fromFragment(), fromView() が用意されている。


2021年12月9日木曜日

moshi を Kotlin で使うときは @Json ではなく @field:Json を使う

moshi では、property の名前を json の field 名と別にしたい場合 @Json アノテーションを使いますが、@Json ではなく @field:Json を使うようにします。 class Player { @field:Json(name = "lucky number") val luckyNumber: Int ... } R8 / ProGuard をかけない場合は @Json でも動くのですが、R8 / ProGuard をかける場合は(ライブラリに含まれる keep 設定では) @field:Json にしないとこの指定が効かず、上記のコードだと実行時に luckyNumber にアクセスしたときに NullPointerException になります。