2021年10月28日木曜日

遷移先情報(deep link とか)を持つ Intent の処理タイミング

以下では、SingleActivity で画面遷移は Navigation Compose を使っている前提である。


ポイント1 : Activity 再生成時には処理しないように onCreate() では savedInstanceState をチェックする

Navigation Compose の backstack 情報は再生成に restore される class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { ... } if (savedInstanceState == null) { handleIntent(intent) } } override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) intent?.let { handleIntent(it) } } ... }

ポイント2 : Recent Apps から起動されたときは処理しないように FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY をチェックする

class MainActivity : ComponentActivity() { ... private fun handleIntent(intent: Intent) { if (intent.flags and Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY != 0) { return } // intent の内容に応じて NavHostController.navigate() する } } Task が生きているときに Recent Apps からアプリを開いても FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY はつかない

Root の Activity が finish されたり、プロセスが kill されているときに Recent Apps からアプリを開くと FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY がつく



2021年9月5日日曜日

Jeptack Compose : DrawScope に描画したものを Bitmap にする。Bitmap を DrawScope に描画する。

ImageBitmap から Bitmap への変換には ImageBitmap.asAndroidBitmap() を使います。
Bitmap から ImageBitmap への変換には Bitmap.asImageBitmap() を使います。

@Preview @Composable fun SampleScreen() { val density = LocalDensity.current val layoutDirection = LocalLayoutDirection.current val bitmap = remember { mutableStateOf<Bitmap?>(null) } Column { Button( onClick = { bitmap.value = createBitmap(density, layoutDirection) }, modifier = Modifier.padding(16.dp) ) { Text(text = "click") } AndroidView( factory = { context -> ImageView(context) }, update = { imageView -> imageView.setImageBitmap(bitmap.value) }, modifier = Modifier.padding(16.dp) ) androidx.compose.foundation.Canvas( modifier = Modifier.padding(16.dp) .size(with(LocalDensity.current) { 512.toDp() }) ) { val bmp = bitmap.value if (bmp != null) { drawImage(bmp.asImageBitmap()) drawCircle(Color.White, radius = size.minDimension / 4f) } } } } private fun createBitmap( density: Density, layoutDirection: LayoutDirection ): Bitmap { val targetSize = 512 val imageBitmap = ImageBitmap(targetSize, targetSize) val size = Size(targetSize.toFloat(), targetSize.toFloat()) CanvasDrawScope().draw(density, layoutDirection, Canvas(imageBitmap), size) { drawCircle(Color.Red) } return imageBitmap.asAndroidBitmap() }

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 あり/なしの比較をしたいので、共通する部分のコードを先に出しておきます。 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 をクリアしています。