2021年8月29日日曜日

navigation-compose は ViewModel のライフサイクルをどう管理しているのか

twitter で言及したこれの解説です。

使用ライブラリ

以下の解説は

compose_version : 1.0.1
androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha07
androidx.navigation:navigation-compose:2.4.0-alpha07
のコードをもとにしています。

内部の実装は今後変わる可能性があります。

共通コード

navigation-compose あり/なしの比較をしたいので、共通する部分のコードを先に出しておきます。
  1. class Screen1ViewModel : ViewModel() {  
  2.   
  3.     val values: List<Int> = (0 until 20).map { Random.nextInt() }  
  4.   
  5.     init {  
  6.         println("Screen1ViewModel : created : $this")  
  7.     }  
  8.   
  9.     override fun onCleared() {  
  10.         println("Screen1ViewModel : cleared : $this")  
  11.     }  
  12. }  
  13.   
  14. class Screen2ViewModel : ViewModel() {  
  15.   
  16.     val value = Random.nextFloat()  
  17.   
  18.     init {  
  19.         println("Screen2ViewModel : created : $this")  
  20.     }  
  21.   
  22.     override fun onCleared() {  
  23.         println("Screen2ViewModel : cleared : $this")  
  24.     }  
  25. }  
  26.   
  27.   
  28. @Composable  
  29. fun Screen1(  
  30.     viewModel: Screen1ViewModel,  
  31.     onClickItem: (Int) -> Unit,  
  32. ) {  
  33.     DisposableEffect(Unit) {  
  34.         println("Screen1 : composed : viewModel = $viewModel")  
  35.         onDispose {  
  36.             println("Screen1 : disposed")  
  37.         }  
  38.     }  
  39.     LazyColumn(modifier = Modifier.fillMaxSize()) {  
  40.         items(viewModel.values) { value ->  
  41.             Text(  
  42.                 text = value.toString(),  
  43.                 modifier = Modifier  
  44.                     .fillMaxWidth()  
  45.                     .clickable {  
  46.                         onClickItem(value)  
  47.                     }  
  48.                     .padding(16.dp)  
  49.             )  
  50.         }  
  51.     }  
  52. }  
  53.   
  54. @Composable  
  55. fun Screen2(  
  56.     viewModel: Screen2ViewModel,  
  57.     value1: Int  
  58. ) {  
  59.     DisposableEffect(Unit) {  
  60.         println("Screen2 : composed : viewModel = $viewModel")  
  61.         onDispose {  
  62.             println("Screen2 : disposed")  
  63.         }  
  64.     }  
  65.     Text(  
  66.         text = "value1 = $value1, value2 = ${viewModel.value}",  
  67.         modifier = Modifier  
  68.             .fillMaxWidth()  
  69.             .padding(16.dp)  
  70.     )  
  71. }  
  72.   
  73.   
  74. class MainActivity : ComponentActivity() {  
  75.     override fun onCreate(savedInstanceState: Bundle?) {  
  76.         super.onCreate(savedInstanceState)  
  77.         setContent {  
  78.             MaterialTheme {  
  79.                 Surface(color = MaterialTheme.colors.background) {  
  80.                     MyApp()  
  81.                 }  
  82.             }  
  83.         }  
  84.     }  
  85. }  

navigation-compose を使わないときの ViewModel のライフサイクル

まず navigation-compose を使わないときの動作を確認してみましょう。
  1. @Composable  
  2. fun MyApp() {  
  3.     val screenState = remember { mutableStateOf<Screen>(Screen.Screen1) }  
  4.   
  5.     when (val screen = screenState.value) {  
  6.         Screen.Screen1 -> {  
  7.             Screen1(viewModel()) {  
  8.                 screenState.value = Screen.Screen2(it)  
  9.             }  
  10.         }  
  11.         is Screen.Screen2 -> {  
  12.             BackHandler {  
  13.                 screenState.value = Screen.Screen1  
  14.             }  
  15.             Screen2(viewModel(), screen.value)  
  16.         }  
  17.     }  
  18. }  
  1. Screen1ViewModel : created : com.sample.myapplication.Screen1ViewModel@7826ac6  
  2. Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@7826ac6  
  3.   
  4. -- Screen1 の LazyColumn のアイテムをタップ  
  5.   
  6. Screen2ViewModel : created : com.sample.myapplication.Screen2ViewModel@4d64aba  
  7. Screen1 : disposed  
  8. Screen2 : composed : viewModel = com.sample.myapplication.Screen2ViewModel@4d64aba  
  9.   
  10. -- back キータップ  
  11.   
  12. Screen2 : disposed  
  13. Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@7826ac6  
  14.   
  15. -- Screen1 の LazyColumn のアイテムをタップ  
  16.   
  17. Screen1 : disposed  
  18. Screen2 : composed : viewModel = com.sample.myapplication.Screen2ViewModel@4d64aba  
  19.   
  20. -- back キータップ  
  21.   
  22. Screen2 : disposed  
  23. Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@7826ac6  
  24.   
  25. -- back キータップ(アプリ終了)  
  26.   
  27. Screen1 : disposed  
  28. Screen1ViewModel : cleared : com.sample.myapplication.Screen1ViewModel@7826ac6  
  29. Screen2ViewModel : cleared : com.sample.myapplication.Screen2ViewModel@4d64aba  
Screen1ViewModel、Screen2ViewModel いずれも1回しか生成されておらず、画面上の表示は Screen1 → Screen2 → Screen1 → Screen2 → Screen1 と遷移していますが、viewModel() では 同じ ViewModel インスタンスが返ってきていることがわかります。

viewModel() は androidx.lifecycle:lifecycle-viewmodel-compose に定義されている拡張関数で、次のようになっています。
  1. @Composable  
  2. public inline fun <reified VM : ViewModel> viewModel(  
  3.     viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {  
  4.         "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"  
  5.     },  
  6.     key: String? = null,  
  7.     factory: ViewModelProvider.Factory? = null  
  8. ): VM = viewModel(VM::class.java, viewModelStoreOwner, key, factory)  
  9.   
  10. @Composable  
  11. public fun <VM : ViewModel> viewModel(  
  12.     modelClass: Class<VM>,  
  13.     viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {  
  14.         "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"  
  15.     },  
  16.     key: String? = null,  
  17.     factory: ViewModelProvider.Factory? = null  
  18. ): VM = viewModelStoreOwner.get(modelClass, key, factory)  
  19.   
  20. private fun <VM : ViewModel> ViewModelStoreOwner.get(  
  21.     javaClass: Class<VM>,  
  22.     key: String? = null,  
  23.     factory: ViewModelProvider.Factory? = null  
  24. ): VM {  
  25.     val provider = if (factory != null) {  
  26.         ViewModelProvider(this, factory)  
  27.     } else {  
  28.         ViewModelProvider(this)  
  29.     }  
  30.     return if (key != null) {  
  31.         provider.get(key, javaClass)  
  32.     } else {  
  33.         provider.get(javaClass)  
  34.     }  
  35. }  
LocalViewModelStoreOwner.current から ViewModelStoreOwner を取得し、ViewModelProvider() にそれを渡して ViewModel のインスタンスを取得しています。

ViewModelProvider() に ViewModelStoreOwner を渡して ViewModel のインスタンスを取得するのは、今まで Activity や Fragment でやっていたのと同じです。 Activity や Fragment は ViewModelStoreOwner を実装しています。

では Screen1 や Screen2 で viewModel() を呼ぶとき LocalViewModelStoreOwner には何がセットされているのでしょうか。
  1. Screen.Screen1 -> {  
  2.     println("Screen1 : ViewModelStoreOwner = ${LocalViewModelStoreOwner.current}")  
  3.     Screen1(viewModel()) {  
  4.         screenState.value = Screen.Screen2(it)  
  5.     }  
  6. }  
  1. Screen1 : ViewModelStoreOwner = com.sample.myapplication.MainActivity@7826ac6  
  2. ...  
  3. Screen2 : ViewModelStoreOwner = com.sample.myapplication.MainActivity@7826ac6  
LocalViewModelStoreOwner には MainActivity が入っていることがわかりました。

つまり Screen1ViewModel、Screen2ViewModel いずれも MainActivity に紐づいており、表示上は Screen1, Screen2 と遷移していますが、常に同じ MainActivity にいるので viewModel() で同じインスタンスが返ってきていたということだったのです。

navigation-compose での ViewModel のライフサイクル

navigation-compose を使った場合どうなるのか確認してみましょう。
  1. @Composable  
  2. fun MyApp() {  
  3.     val navController = rememberNavController()  
  4.   
  5.     NavHost(navController = navController, startDestination = "Screen1") {  
  6.         composable(  
  7.             route = "Screen1"  
  8.         ) {  
  9.             println("Screen1 : ViewModelStoreOwner = ${LocalViewModelStoreOwner.current}")  
  10.             Screen1(viewModel()) {  
  11.                 navController.navigate("Screen2/$it")  
  12.             }  
  13.         }  
  14.         composable(  
  15.             route = "Screen2/{value}",  
  16.             arguments = listOf(navArgument("value") { type = NavType.IntType })  
  17.         ) {  
  18.             val value1 = requireNotNull(it.arguments).getInt("value")  
  19.             println("Screen2 : ViewModelStoreOwner = ${LocalViewModelStoreOwner.current}")  
  20.             Screen2(viewModel(), value1)  
  21.         }  
  22.     }  
  23. }  
  1. Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb  
  2. Screen1ViewModel : created : com.sample.myapplication.Screen1ViewModel@f79f3f3  
  3. Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@f79f3f3  
  4. Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb  
  5.   
  6. -- Screen1 の LazyColumn のアイテムをタップ  
  7.   
  8. Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb  
  9. Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@861e3e2a  
  10. Screen2ViewModel : created : com.sample.myapplication.Screen2ViewModel@7cf0642  
  11. Screen2 : composed : viewModel = com.sample.myapplication.Screen2ViewModel@7cf0642  
  12. Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb  
  13. Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@861e3e2a  
  14. Screen1 : disposed  
  15. Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@861e3e2a  
  16.   
  17. -- back キータップ   
  18.   
  19. Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@861e3e2a  
  20. Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb  
  21. Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@f79f3f3  
  22. Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@861e3e2a  
  23. Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb  
  24. Screen2ViewModel : cleared : com.sample.myapplication.Screen2ViewModel@7cf0642  
  25. Screen2 : disposed  
  26. Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb  
  27.   
  28. -- Screen1 の LazyColumn のアイテムをタップ  
  29.   
  30. Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb  
  31. Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@27c16dd1  
  32. Screen2ViewModel : created : com.sample.myapplication.Screen2ViewModel@96e0d3b  
  33. Screen2 : composed : viewModel = com.sample.myapplication.Screen2ViewModel@96e0d3b  
  34. Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb  
  35. Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@27c16dd1  
  36. Screen1 : disposed  
  37. Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@27c16dd1  
  38.   
  39. -- back キータップ   
  40.   
  41. Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@27c16dd1  
  42. Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb  
  43. Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@f79f3f3  
  44. Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@27c16dd1  
  45. Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb  
  46. Screen2ViewModel : cleared : com.sample.myapplication.Screen2ViewModel@96e0d3b  
  47. Screen2 : disposed  
  48. Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb  
  49.   
  50. -- back キータップ(アプリ終了)  
  51.   
  52. Screen1 : disposed  
  53. Screen1ViewModel : cleared : com.sample.myapplication.Screen1ViewModel@f79f3f3  
LocalViewModelStoreOwner には MainActivity ではなく NavBackStackEntry が入っていることがわかりました。
ViewModelStoreOwner の部分を省くと
  1. Screen1ViewModel : created : com.sample.myapplication.Screen1ViewModel@f79f3f3  
  2. Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@f79f3f3  
  3.   
  4. -- Screen1 の LazyColumn のアイテムをタップ  
  5.   
  6. Screen2ViewModel : created : com.sample.myapplication.Screen2ViewModel@7cf0642  
  7. Screen2 : composed : viewModel = com.sample.myapplication.Screen2ViewModel@7cf0642  
  8. Screen1 : disposed  
  9.   
  10. -- back キータップ = popBackstack  
  11.   
  12. Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@f79f3f3  
  13. Screen2ViewModel : cleared : com.sample.myapplication.Screen2ViewModel@7cf0642  
  14. Screen2 : disposed  
  15.   
  16. -- Screen1 の LazyColumn のアイテムをタップ  
  17.   
  18. Screen2ViewModel : created : com.sample.myapplication.Screen2ViewModel@96e0d3b  
  19. Screen2 : composed : viewModel = com.sample.myapplication.Screen2ViewModel@96e0d3b  
  20. Screen1 : disposed  
  21.   
  22. -- back キータップ = popBackstack  
  23.   
  24. Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@f79f3f3  
  25. Screen2ViewModel : cleared : com.sample.myapplication.Screen2ViewModel@96e0d3b  
  26. Screen2 : disposed  
  27.   
  28. -- back キータップ(アプリ終了)  
  29.   
  30. Screen1 : disposed  
  31. Screen1ViewModel : cleared : com.sample.myapplication.Screen1ViewModel@f79f3f3  
Screen1 → Screen2 → Screen1 のとき(Screen2 が backstack からいなくなった)に Screen2ViewModel が破棄(onCleared)され、再度 Screen2 に遷移したときに別の Screen2ViewModel インスタンスが作られていることがわかります。

LocalViewModelStoreOwner へのセット

navigation-compose では NavHost composable で LocalViewModelStoreOwner に NavBackStackEntry をセットしています。
  1. @Composable  
  2. public fun NavBackStackEntry.LocalOwnersProvider(  
  3.     saveableStateHolder: SaveableStateHolder,  
  4.     content: @Composable () -> Unit  
  5. ) {  
  6.     CompositionLocalProvider(  
  7.         LocalViewModelStoreOwner provides this,  
  8.         LocalLifecycleOwner provides this,  
  9.         LocalSavedStateRegistryOwner provides this  
  10.     ) {  
  11.         saveableStateHolder.SaveableStateProvider(content)  
  12.     }  
  13. }  
  14.     
  15. @Composable  
  16. public fun NavHost(  
  17.     navController: NavHostController,  
  18.     graph: NavGraph,  
  19.     modifier: Modifier = Modifier  
  20. ) {  
  21.     ...  
  22.     if (backStackEntry != null) {  
  23.         ...  
  24.         Crossfade(backStackEntry.id, modifier) {  
  25.             val lastEntry = transitionsInProgress.lastOrNull { entry ->  
  26.                 it == entry.id  
  27.             } ?: backStack.lastOrNull { entry ->  
  28.                 it == entry.id  
  29.             }  
  30.   
  31.             lastEntry?.LocalOwnersProvider(saveableStateHolder) {  
  32.                 (lastEntry.destination as ComposeNavigator.Destination).content(lastEntry)  
  33.             }  
  34.             ...  
  35.         }  
  36.     }  
  37.   
  38.     ...  
  39. }  
backStackEntry には backstack の一番最後のものが入ります。

NavBackStackEntry の ViewModelStoreOwner 実装

では NavBackStackEntry が ViewModelStoreOwner をどのように実装しているのか見てみましょう。
  1. public class NavBackStackEntry private constructor(  
  2.     ...  
  3.     private val viewModelStoreProvider: NavViewModelStoreProvider? = null,  
  4.     ...  
  5. ) : LifecycleOwner,  
  6.     ViewModelStoreOwner,  
  7.     HasDefaultViewModelProviderFactory,  
  8.     SavedStateRegistryOwner {  
  9.   
  10.     ...  
  11.     
  12.     public override fun getViewModelStore(): ViewModelStore {  
  13.         ...  
  14.         checkNotNull(viewModelStoreProvider) {  
  15.              ...  
  16.         }  
  17.         return viewModelStoreProvider.getViewModelStore(id)  
  18.     }    
  19. }  
NavBackStackEntry の ViewModelStoreOwner 実装では NavViewModelStoreProvider から ViewModelStore を取得しています。

NavViewModelStoreProvider は interface で、backStackEntryId: String から ViewModelStore を返すメソッドが定義されています。
  1. @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)  
  2. public interface NavViewModelStoreProvider {  
  3.     public fun getViewModelStore(backStackEntryId: String): ViewModelStore  
  4. }  
NavViewModelStoreProvider を実装しているのは NavControllerViewModel です。
  1. internal class NavControllerViewModel : ViewModel(), NavViewModelStoreProvider {  
  2.     private val viewModelStores = mutableMapOf<String, ViewModelStore>()  
  3.   
  4.     fun clear(backStackEntryId: String) {  
  5.         // Clear and remove the NavGraph's ViewModelStore  
  6.         val viewModelStore = viewModelStores.remove(backStackEntryId)  
  7.         viewModelStore?.clear()  
  8.     }  
  9.   
  10.     override fun onCleared() {  
  11.         for (store in viewModelStores.values) {  
  12.             store.clear()  
  13.         }  
  14.         viewModelStores.clear()  
  15.     }  
  16.   
  17.     override fun getViewModelStore(backStackEntryId: String): ViewModelStore {  
  18.         var viewModelStore = viewModelStores[backStackEntryId]  
  19.         if (viewModelStore == null) {  
  20.             viewModelStore = ViewModelStore()  
  21.             viewModelStores[backStackEntryId] = viewModelStore  
  22.         }  
  23.         return viewModelStore  
  24.     }  
  25.   
  26.     ...  
  27.   
  28.     companion object {  
  29.         private val FACTORY: ViewModelProvider.Factory = object : ViewModelProvider.Factory {  
  30.             @Suppress("UNCHECKED_CAST")  
  31.             override fun <T : ViewModel> create(modelClass: Class<T>): T {  
  32.                 return NavControllerViewModel() as T  
  33.             }  
  34.         }  
  35.   
  36.         @JvmStatic  
  37.         fun getInstance(viewModelStore: ViewModelStore): NavControllerViewModel {  
  38.             val viewModelProvider = ViewModelProvider(viewModelStore, FACTORY)  
  39.             return viewModelProvider.get()  
  40.         }  
  41.     }  
  42. }  
NavControllerViewModel は backStackEntryId: String と ViewModelStore の Map を持っています。

NavControllerViewModel は NavHost composable が呼び出される時点での LocalViewModelStoreOwner にセットされている ViewModelStoreOwner を使って生成され、NavController にセットされます。
  1. @Composable  
  2. public fun NavHost(  
  3.     navController: NavHostController,  
  4.     graph: NavGraph,  
  5.     modifier: Modifier = Modifier  
  6. ) {  
  7.     val lifecycleOwner = LocalLifecycleOwner.current  
  8.     val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {  
  9.         "NavHost requires a ViewModelStoreOwner to be provided via LocalViewModelStoreOwner"  
  10.     }  
  11.   
  12.     ...  
  13.   
  14.     navController.setViewModelStore(viewModelStoreOwner.viewModelStore)  
  15.   
  16.     ...  
  17. }  
  18.   
  19. public open class NavHostController(context: Context) : NavController(context) {  
  20.     ...  
  21.     public final override fun setViewModelStore(viewModelStore: ViewModelStore) {  
  22.         super.setViewModelStore(viewModelStore)  
  23.     }  
  24. }  
  25.   
  26.     
  27. public open class NavController(  
  28.     ...  
  29. ) {  
  30.     ...  
  31.   
  32.     @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)  
  33.     public open fun setViewModelStore(viewModelStore: ViewModelStore) {  
  34.         if (viewModel == NavControllerViewModel.getInstance(viewModelStore)) {  
  35.             return  
  36.         }  
  37.         check(backQueue.isEmpty()) { "ViewModelStore should be set before setGraph call" }  
  38.         viewModel = NavControllerViewModel.getInstance(viewModelStore)  
  39.     }  
  40.     ...  
  41. }  
NavController にセットされた NavControllerViewModel は、NavBackStackEntry を生成するときに NavViewModelStoreProvider として渡されます。

つまり NavBackStackEntry を ViewModelStoreOwner として ViewModel を取得する場合、NavControllerViewModel に ViewModelStore が作られ、対応する backStackEntryId との Map で保持されます。

NavController では NavBackStackEntry が backstack から pop されたときに、NavControllerViewModel で保持されている対応する ViewModelStore をクリアしています。


0 件のコメント:

コメントを投稿