■ きっかけ
普通に、トップレベルの Composable内で State/Flow/StateFlow を返す ViewModel に接続します。
Composable ↔ ViewModel (↔ Repository)
一般的な NavHostController を使う場合には、それは NavBackStackEntryインスタンス となり 一つの Activity の上にスタック上に積まれていく。
👉 Composable の LifecycleOwner は誰なのか - collectAsStateWithLifecycle
NavBackStackEntry3 - Composable3 ↔ ViewModel3
NavBackStackEntry2 - Composable2 ↔ ViewModel2
NavBackStackEntry1 - Composable1 ↔ ViewModel1
Activity
ライフサイクル上、Activity と NavBackStackEntry との関係は良さそうです。
外側のActivityやFragmentがonStop状態になっていればこのNavBackStackEntryのlifecycleはCREATED状態になってくれていそうにみえます。
👉 ComposeNavigationで使えるNavBackStackEntryのlifecycleとは?
👉 NavBackStackEntryのLifecycleについて | by Kenji Abe | Medium
Flow を扱う ViewModel と、
スタックされる NavBackStackEntry (Screen-Level Composable) のライフサイクル。
気になります。
目視したいです。
遷移によっては同じ Composable がスタックに積まれていくのもキモいし。
■ 方法
ViewModel の生き死には、
class TodoViewModel @Inject constructor(
) : ViewModel() {
init {
// log
}
override fun onCleared() {
// log
}
}
で確認します。
👉 ViewModel はいつ生まれていつ死ぬか 【→ Jetpack Compose】
Composable - NavBackStackEntry のライフサイクルは、
LifecycleEventObserver { _, event ->
// log event
}
を使います。hash 付けてます。
👉 DisposableEffect: クリーンアップが必要な作用
遷移先となる Composale に貼ります。
@Composable
fun TodoScreen(
viewModel: TodoViewModel = hiltViewModel()
) {
LocalLifecycleStateLogger(" TodoScreen") // *
@Composable
fun PokeScreen(
viewModel: TodoViewModel = hiltViewModel()
) {
LocalLifecycleStateLogger(" PokeScreen") // *
ついでに、これらの親となる NavHostController をセットしている Composale にも貼ります。
@Composable
fun MainScreen() {
SystemUi(rememberSystemUiController())
LocalLifecycleStateLogger(screenName = "MainScreen") // *
val navHostController = rememberNavController()
■ 結果
BottomNavigation をつかっていますが、スタックや saveState の設定はデフォルトのままなので、navigation を使っている場合は同様ではないかと思います。
アプリ起動
↓ ① click
TodoScreen
↓ ② click
PokeScreen
↓ ③ click
TodoScreen
↓ ④ back
PokeScreen
↓ ⑤ back
TodoScreen
↓ ⑥ back
アプリ終了
ログ出力結果。
D: @@@ MainScreen - MainActivity@a542ab9 -> ON_CREATE
D: @@@ MainScreen - MainActivity@a542ab9 -> ON_START
D: @@@ MainScreen - MainActivity@a542ab9 -> ON_RESUME
D: @@@ TodoViewModel@abae21d initialized.
D: @@@ TodoScreen - NavBackStackEntry@8ad648d7 -> ON_CREATE
D: @@@ TodoScreen - NavBackStackEntry@8ad648d7 -> ON_START
D: @@@ TodoScreen - NavBackStackEntry@8ad648d7 -> ON_RESUME
D: @@@ TodoScreen - NavBackStackEntry@8ad648d7 -> ON_PAUSE
D: @@@ TodoScreen - NavBackStackEntry@8ad648d7 -> ON_STOP
D: @@@ PokeViewModel@ad0cefd initialized.
D: @@@ PokeScreen - NavBackStackEntry@4049e1c3 -> ON_CREATE
D: @@@ PokeScreen - NavBackStackEntry@4049e1c3 -> ON_START
D: @@@ PokeScreen - NavBackStackEntry@4049e1c3 -> ON_RESUME
D: @@@ PokeScreen - NavBackStackEntry@4049e1c3 -> ON_PAUSE
D: @@@ PokeScreen - NavBackStackEntry@4049e1c3 -> ON_STOP
D: @@@ TodoViewModel@577b5b1 initialized.
D: @@@ TodoScreen - NavBackStackEntry@f1d51055 -> ON_CREATE
D: @@@ TodoScreen - NavBackStackEntry@f1d51055 -> ON_START
D: @@@ TodoScreen - NavBackStackEntry@f1d51055 -> ON_RESUME
D: @@@ TodoScreen - NavBackStackEntry@f1d51055 -> ON_PAUSE
D: @@@ TodoScreen - NavBackStackEntry@f1d51055 -> ON_STOP
D: @@@ PokeScreen - NavBackStackEntry@4049e1c3 -> ON_CREATE
D: @@@ PokeScreen - NavBackStackEntry@4049e1c3 -> ON_START
D: @@@ TodoScreen - NavBackStackEntry@f1d51055 -> ON_DESTROY
D: @@@ TodoViewModel@577b5b1 onCleared.
D: @@@ PokeScreen - NavBackStackEntry@4049e1c3 -> ON_RESUME
D: @@@ PokeScreen - NavBackStackEntry@4049e1c3 -> ON_PAUSE
D: @@@ PokeScreen - NavBackStackEntry@4049e1c3 -> ON_STOP
D: @@@ TodoScreen - NavBackStackEntry@8ad648d7 -> ON_CREATE
D: @@@ TodoScreen - NavBackStackEntry@8ad648d7 -> ON_START
D: @@@ PokeScreen - NavBackStackEntry@4049e1c3 -> ON_DESTROY
D: @@@ PokeViewModel@ad0cefd onCleared.
D: @@@ TodoScreen - NavBackStackEntry@8ad648d7 -> ON_RESUME
D: @@@ TodoScreen - NavBackStackEntry@8ad648d7 -> ON_PAUSE
D: @@@ MainScreen - MainActivity@a542ab9 -> ON_PAUSE
D: @@@ TodoScreen - NavBackStackEntry@8ad648d7 -> ON_STOP
D: @@@ MainScreen - MainActivity@a542ab9 -> ON_STOP
ログから 各Screen = NavBackStackEntry のスタック状態を考えます。
-
↓ ① click
TodoScreen1 ↔ TodoViewModel1
MainScreen1 - MainActivity
↓ ② click
PokeScreen1 ↔ PokeViewModel1
TodoScreen1 ↔ TodoViewModel1
MainScreen1 - MainActivity
↓ ③ click
TodoScreen2 ↔ TodoViewModel2
PokeScreen1 ↔ PokeViewModel1
TodoScreen1 ↔ TodoViewModel1
MainScreen@1 - MainActivity
↓ ④ back
PokeScreen1 ↔ TodoViewModel2
TodoScreen1 ↔ TodoViewModel1
MainScreen1 - MainActivity
↓ ⑤ back
TodoScreen1 ↔ TodoViewModel1
MainScreen1 - MainActivity
↓ ⑥ back
-
それぞれの NavBackStackEntry のライフサイクルと ViewModel の状態が確認できました。
ホストの Composable のライフサイクルオーナーは、Activity であることも分かりました。
画面が遷移していくと、NavBackStackEntry はスタックされていき、 back (戻る操作) すると、NavBackStackEntry は ON_DESTROY され、スタックから取り除くかれ、それに対応する ViewModel も onCleard() されていきます。
なんとなく思うのは、今現在としては、ViewModel は、やっぱりあったほうがいいように思います。
👉 【Jetpack Compose】 ViewModel を捨てて Repository を Composable に直結する
そのうち、
hiltViewModel<ViewModel>()
のように、Reposiitory や DataProvider を注入する
androidx.hilt.navigation.compose メソッドやアノテーションが出れば嬉しいです。
👉 Jetpack Compose without ViewModel / Twitter
👉 Jim Sproch (@JimSproch) - Jetpack Composeのコンポーネントはなぜ返り値がないのか - Blog - Mori Atsushi
もうこれは「UIもマルチスレッドな時代になった」と言っていいんすかね。
👉 User interface frameworks typically are single-threaded. Why is this so and what are the performance implications of this single-threading? - Quora
👉 Are Multiple UI Threads Possible? | Apple Developer Forums
(つづく...)