ViewModel が Compose のライフサイクルと対応してないって言ってる正確な意味はわからないけど、SingleActivity + full Compose で、Compose で作った1画面を今までの Activity や Fragment のように使いたいなら navigation-compose 使うのがいいと思う。https://t.co/fYTMoZqTLm
— Yuki Anzai (@yanzm) August 26, 2021
使用ライブラリ
以下の解説はcompose_version : 1.0.1
androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha07
androidx.navigation:navigation-compose:2.4.0-alpha07
のコードをもとにしています。
内部の実装は今後変わる可能性があります。
共通コード
navigation-compose あり/なしの比較をしたいので、共通する部分のコードを先に出しておきます。
class Screen1ViewModel : ViewModel() {
val values: List<Int> = (0 until 20).map { Random.nextInt() }
init {
println("Screen1ViewModel : created : $this")
}
override fun onCleared() {
println("Screen1ViewModel : cleared : $this")
}
}
class Screen2ViewModel : ViewModel() {
val value = Random.nextFloat()
init {
println("Screen2ViewModel : created : $this")
}
override fun onCleared() {
println("Screen2ViewModel : cleared : $this")
}
}
@Composable
fun Screen1(
viewModel: Screen1ViewModel,
onClickItem: (Int) -> Unit,
) {
DisposableEffect(Unit) {
println("Screen1 : composed : viewModel = $viewModel")
onDispose {
println("Screen1 : disposed")
}
}
LazyColumn(modifier = Modifier.fillMaxSize()) {
items(viewModel.values) { value ->
Text(
text = value.toString(),
modifier = Modifier
.fillMaxWidth()
.clickable {
onClickItem(value)
}
.padding(16.dp)
)
}
}
}
@Composable
fun Screen2(
viewModel: Screen2ViewModel,
value1: Int
) {
DisposableEffect(Unit) {
println("Screen2 : composed : viewModel = $viewModel")
onDispose {
println("Screen2 : disposed")
}
}
Text(
text = "value1 = $value1, value2 = ${viewModel.value}",
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
)
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Surface(color = MaterialTheme.colors.background) {
MyApp()
}
}
}
}
}
navigation-compose を使わないときの ViewModel のライフサイクル
まず navigation-compose を使わないときの動作を確認してみましょう。
@Composable
fun MyApp() {
val screenState = remember { mutableStateOf<Screen>(Screen.Screen1) }
when (val screen = screenState.value) {
Screen.Screen1 -> {
Screen1(viewModel()) {
screenState.value = Screen.Screen2(it)
}
}
is Screen.Screen2 -> {
BackHandler {
screenState.value = Screen.Screen1
}
Screen2(viewModel(), screen.value)
}
}
}
Screen1ViewModel : created : com.sample.myapplication.Screen1ViewModel@7826ac6
Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@7826ac6
-- Screen1 の LazyColumn のアイテムをタップ
Screen2ViewModel : created : com.sample.myapplication.Screen2ViewModel@4d64aba
Screen1 : disposed
Screen2 : composed : viewModel = com.sample.myapplication.Screen2ViewModel@4d64aba
-- back キータップ
Screen2 : disposed
Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@7826ac6
-- Screen1 の LazyColumn のアイテムをタップ
Screen1 : disposed
Screen2 : composed : viewModel = com.sample.myapplication.Screen2ViewModel@4d64aba
-- back キータップ
Screen2 : disposed
Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@7826ac6
-- back キータップ(アプリ終了)
Screen1 : disposed
Screen1ViewModel : cleared : com.sample.myapplication.Screen1ViewModel@7826ac6
Screen2ViewModel : cleared : com.sample.myapplication.Screen2ViewModel@4d64aba
Screen1ViewModel、Screen2ViewModel いずれも1回しか生成されておらず、画面上の表示は Screen1 → Screen2 → Screen1 → Screen2 → Screen1 と遷移していますが、viewModel() では 同じ ViewModel インスタンスが返ってきていることがわかります。
viewModel() は androidx.lifecycle:lifecycle-viewmodel-compose に定義されている拡張関数で、次のようになっています。
@Composable
public inline fun <reified VM : ViewModel> viewModel(
viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
},
key: String? = null,
factory: ViewModelProvider.Factory? = null
): VM = viewModel(VM::class.java, viewModelStoreOwner, key, factory)
@Composable
public fun <VM : ViewModel> viewModel(
modelClass: Class<VM>,
viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
},
key: String? = null,
factory: ViewModelProvider.Factory? = null
): VM = viewModelStoreOwner.get(modelClass, key, factory)
private fun <VM : ViewModel> ViewModelStoreOwner.get(
javaClass: Class<VM>,
key: String? = null,
factory: ViewModelProvider.Factory? = null
): VM {
val provider = if (factory != null) {
ViewModelProvider(this, factory)
} else {
ViewModelProvider(this)
}
return if (key != null) {
provider.get(key, javaClass)
} else {
provider.get(javaClass)
}
}
LocalViewModelStoreOwner.current から ViewModelStoreOwner を取得し、ViewModelProvider() にそれを渡して ViewModel のインスタンスを取得しています。
ViewModelProvider() に ViewModelStoreOwner を渡して ViewModel のインスタンスを取得するのは、今まで Activity や Fragment でやっていたのと同じです。 Activity や Fragment は ViewModelStoreOwner を実装しています。
では Screen1 や Screen2 で viewModel() を呼ぶとき LocalViewModelStoreOwner には何がセットされているのでしょうか。
Screen.Screen1 -> {
println("Screen1 : ViewModelStoreOwner = ${LocalViewModelStoreOwner.current}")
Screen1(viewModel()) {
screenState.value = Screen.Screen2(it)
}
}
Screen1 : ViewModelStoreOwner = com.sample.myapplication.MainActivity@7826ac6
...
Screen2 : ViewModelStoreOwner = com.sample.myapplication.MainActivity@7826ac6
LocalViewModelStoreOwner には MainActivity が入っていることがわかりました。
つまり Screen1ViewModel、Screen2ViewModel いずれも MainActivity に紐づいており、表示上は Screen1, Screen2 と遷移していますが、常に同じ MainActivity にいるので viewModel() で同じインスタンスが返ってきていたということだったのです。
navigation-compose での ViewModel のライフサイクル
navigation-compose を使った場合どうなるのか確認してみましょう。
@Composable
fun MyApp() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "Screen1") {
composable(
route = "Screen1"
) {
println("Screen1 : ViewModelStoreOwner = ${LocalViewModelStoreOwner.current}")
Screen1(viewModel()) {
navController.navigate("Screen2/$it")
}
}
composable(
route = "Screen2/{value}",
arguments = listOf(navArgument("value") { type = NavType.IntType })
) {
val value1 = requireNotNull(it.arguments).getInt("value")
println("Screen2 : ViewModelStoreOwner = ${LocalViewModelStoreOwner.current}")
Screen2(viewModel(), value1)
}
}
}
Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
Screen1ViewModel : created : com.sample.myapplication.Screen1ViewModel@f79f3f3
Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@f79f3f3
Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
-- Screen1 の LazyColumn のアイテムをタップ
Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@861e3e2a
Screen2ViewModel : created : com.sample.myapplication.Screen2ViewModel@7cf0642
Screen2 : composed : viewModel = com.sample.myapplication.Screen2ViewModel@7cf0642
Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@861e3e2a
Screen1 : disposed
Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@861e3e2a
-- back キータップ
Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@861e3e2a
Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@f79f3f3
Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@861e3e2a
Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
Screen2ViewModel : cleared : com.sample.myapplication.Screen2ViewModel@7cf0642
Screen2 : disposed
Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
-- Screen1 の LazyColumn のアイテムをタップ
Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@27c16dd1
Screen2ViewModel : created : com.sample.myapplication.Screen2ViewModel@96e0d3b
Screen2 : composed : viewModel = com.sample.myapplication.Screen2ViewModel@96e0d3b
Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@27c16dd1
Screen1 : disposed
Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@27c16dd1
-- back キータップ
Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@27c16dd1
Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@f79f3f3
Screen2 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@27c16dd1
Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
Screen2ViewModel : cleared : com.sample.myapplication.Screen2ViewModel@96e0d3b
Screen2 : disposed
Screen1 : ViewModelStoreOwner = androidx.navigation.NavBackStackEntry@ba021bdb
-- back キータップ(アプリ終了)
Screen1 : disposed
Screen1ViewModel : cleared : com.sample.myapplication.Screen1ViewModel@f79f3f3
LocalViewModelStoreOwner には MainActivity ではなく NavBackStackEntry が入っていることがわかりました。
ViewModelStoreOwner の部分を省くと
Screen1ViewModel : created : com.sample.myapplication.Screen1ViewModel@f79f3f3
Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@f79f3f3
-- Screen1 の LazyColumn のアイテムをタップ
Screen2ViewModel : created : com.sample.myapplication.Screen2ViewModel@7cf0642
Screen2 : composed : viewModel = com.sample.myapplication.Screen2ViewModel@7cf0642
Screen1 : disposed
-- back キータップ = popBackstack
Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@f79f3f3
Screen2ViewModel : cleared : com.sample.myapplication.Screen2ViewModel@7cf0642
Screen2 : disposed
-- Screen1 の LazyColumn のアイテムをタップ
Screen2ViewModel : created : com.sample.myapplication.Screen2ViewModel@96e0d3b
Screen2 : composed : viewModel = com.sample.myapplication.Screen2ViewModel@96e0d3b
Screen1 : disposed
-- back キータップ = popBackstack
Screen1 : composed : viewModel = com.sample.myapplication.Screen1ViewModel@f79f3f3
Screen2ViewModel : cleared : com.sample.myapplication.Screen2ViewModel@96e0d3b
Screen2 : disposed
-- back キータップ(アプリ終了)
Screen1 : disposed
Screen1ViewModel : cleared : com.sample.myapplication.Screen1ViewModel@f79f3f3
Screen1 → Screen2 → Screen1 のとき(Screen2 が backstack からいなくなった)に Screen2ViewModel が破棄(onCleared)され、再度 Screen2 に遷移したときに別の Screen2ViewModel インスタンスが作られていることがわかります。
LocalViewModelStoreOwner へのセット
navigation-compose では NavHost composable で LocalViewModelStoreOwner に NavBackStackEntry をセットしています。
@Composable
public fun NavBackStackEntry.LocalOwnersProvider(
saveableStateHolder: SaveableStateHolder,
content: @Composable () -> Unit
) {
CompositionLocalProvider(
LocalViewModelStoreOwner provides this,
LocalLifecycleOwner provides this,
LocalSavedStateRegistryOwner provides this
) {
saveableStateHolder.SaveableStateProvider(content)
}
}
@Composable
public fun NavHost(
navController: NavHostController,
graph: NavGraph,
modifier: Modifier = Modifier
) {
...
if (backStackEntry != null) {
...
Crossfade(backStackEntry.id, modifier) {
val lastEntry = transitionsInProgress.lastOrNull { entry ->
it == entry.id
} ?: backStack.lastOrNull { entry ->
it == entry.id
}
lastEntry?.LocalOwnersProvider(saveableStateHolder) {
(lastEntry.destination as ComposeNavigator.Destination).content(lastEntry)
}
...
}
}
...
}
backStackEntry には backstack の一番最後のものが入ります。
NavBackStackEntry の ViewModelStoreOwner 実装
では NavBackStackEntry が ViewModelStoreOwner をどのように実装しているのか見てみましょう。
public class NavBackStackEntry private constructor(
...
private val viewModelStoreProvider: NavViewModelStoreProvider? = null,
...
) : LifecycleOwner,
ViewModelStoreOwner,
HasDefaultViewModelProviderFactory,
SavedStateRegistryOwner {
...
public override fun getViewModelStore(): ViewModelStore {
...
checkNotNull(viewModelStoreProvider) {
...
}
return viewModelStoreProvider.getViewModelStore(id)
}
}
NavBackStackEntry の ViewModelStoreOwner 実装では NavViewModelStoreProvider から ViewModelStore を取得しています。
NavViewModelStoreProvider は interface で、backStackEntryId: String から ViewModelStore を返すメソッドが定義されています。
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public interface NavViewModelStoreProvider {
public fun getViewModelStore(backStackEntryId: String): ViewModelStore
}
NavViewModelStoreProvider を実装しているのは NavControllerViewModel です。
internal class NavControllerViewModel : ViewModel(), NavViewModelStoreProvider {
private val viewModelStores = mutableMapOf<String, ViewModelStore>()
fun clear(backStackEntryId: String) {
// Clear and remove the NavGraph's ViewModelStore
val viewModelStore = viewModelStores.remove(backStackEntryId)
viewModelStore?.clear()
}
override fun onCleared() {
for (store in viewModelStores.values) {
store.clear()
}
viewModelStores.clear()
}
override fun getViewModelStore(backStackEntryId: String): ViewModelStore {
var viewModelStore = viewModelStores[backStackEntryId]
if (viewModelStore == null) {
viewModelStore = ViewModelStore()
viewModelStores[backStackEntryId] = viewModelStore
}
return viewModelStore
}
...
companion object {
private val FACTORY: ViewModelProvider.Factory = object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return NavControllerViewModel() as T
}
}
@JvmStatic
fun getInstance(viewModelStore: ViewModelStore): NavControllerViewModel {
val viewModelProvider = ViewModelProvider(viewModelStore, FACTORY)
return viewModelProvider.get()
}
}
}
NavControllerViewModel は backStackEntryId: String と ViewModelStore の Map を持っています。
NavControllerViewModel は NavHost composable が呼び出される時点での LocalViewModelStoreOwner にセットされている ViewModelStoreOwner を使って生成され、NavController にセットされます。
@Composable
public fun NavHost(
navController: NavHostController,
graph: NavGraph,
modifier: Modifier = Modifier
) {
val lifecycleOwner = LocalLifecycleOwner.current
val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
"NavHost requires a ViewModelStoreOwner to be provided via LocalViewModelStoreOwner"
}
...
navController.setViewModelStore(viewModelStoreOwner.viewModelStore)
...
}
public open class NavHostController(context: Context) : NavController(context) {
...
public final override fun setViewModelStore(viewModelStore: ViewModelStore) {
super.setViewModelStore(viewModelStore)
}
}
public open class NavController(
...
) {
...
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public open fun setViewModelStore(viewModelStore: ViewModelStore) {
if (viewModel == NavControllerViewModel.getInstance(viewModelStore)) {
return
}
check(backQueue.isEmpty()) { "ViewModelStore should be set before setGraph call" }
viewModel = NavControllerViewModel.getInstance(viewModelStore)
}
...
}
NavController にセットされた NavControllerViewModel は、NavBackStackEntry を生成するときに NavViewModelStoreProvider として渡されます。
つまり NavBackStackEntry を ViewModelStoreOwner として ViewModel を取得する場合、NavControllerViewModel に ViewModelStore が作られ、対応する backStackEntryId との Map で保持されます。
NavController では NavBackStackEntry が backstack から pop されたときに、NavControllerViewModel で保持されている対応する ViewModelStore をクリアしています。