Upgrade to Pro — share decks privately, control downloads, hide ads and more …

プロダクトレベルで必要になる Jetpack Compose テクニック

Avatar for Yuki Anzai Yuki Anzai
October 19, 2021

プロダクトレベルで必要になる Jetpack Compose テクニック

DroidKaigi 2021
https://2021.droidkaigi.jp/

Jetpack Compose でプロダクトレベルのものを作ろうとすると、公式サイトで解説されていないさまざまな手法が必要になってきます。例えば、フォーカスのハンドリングはどうするのか、Material Design Components ではない入力フォームやボタンを作るには、入力フォームがキーボードに被らないようにするには、CameraX を Jetpack Compose で使うには、onResume や onStart のタイミングで処理を行うには、などなど。本講演では、スピーカーが実際に Jetpack Compose でプロダクトを開発するときに必要となった、公式サイトでは解説されていないテクニックを紹介します。

Avatar for Yuki Anzai

Yuki Anzai

October 19, 2021
Tweet

More Decks by Yuki Anzai

Other Decks in Technology

Transcript

  1. "DUJWJUZ௚Լͷ7JFX.PEFMͷϥΠϑαΠΫϧ "DUJWJUZ 4DSFFO" 4DSFFO# 4DSFFO" 4DSFFO# 4DSFFO" 7JFX.PEFM 4DSFFO# 7JFX.PEFM

    4DSFFO" @Composable fun ScreenA( viewModel:ScreenAViewModel = viewModel() ) { … } @Composable fun ScreenB( viewModel:ScreenBViewModel = viewModel() ) { … }
  2. /BWJHBUJPO$PNQPTFͰͷ7JFX.PEFMͷϥΠϑαΠΫϧ 4DSFFO" 4DSFFO# 4DSFFO" 4DSFFO# 4DSFFO" 7JFX.PEFM 4DSFFO# 7JFX.PEFM 4DSFFO"

    /BW)PTU "DUJWJUZ 4DSFFO# 7JFX.PEFM NavHost( … ) { composable("ScreenA") { ScreenA( … ) } composable("ScreenB") { ScreenB( … ) } }
  3. ࣗ෼Ͱ࣮૷͢ΔSEQBSUZMJCSBSZΛ୳͢ w ࣗ෼Ͱ࣮૷͢Δ w άϥϑͱ͔ˠ$BOWBTDPNQPTBCMF .PEJ fi FSESBX99  w

    ಠࣗͷαΠζɾ഑ஔˠ-BZPVUDPNQPTBCMF .PEJ fi FSMBZPVU  w SEQBSUZMJCSBSZ w ྫʣඇಉظը૾ಡΈࠐΈˠDPJM
  4. ભҠઌͷࢦఆΛUZQFTBGFʹ͍ͨ͠ NavHost ( … ) { … composable("item/{id}") { val

    arguments = requireNotNull(it.arguments ) val itemId = ItemId(requireNotNull(arguments.getString("id")) ) ItemDetailScreen ( … ) } } navController.navigate("item/${item.id.value}") /BWJHBUJPO$PNQPTFͰ͸ભҠઌΛจࣈྻͰࢦఆ͢Δ Ͳ͔͜Ͱ
  5. ભҠઌͷࢦఆΛUZQFTBGFʹ͍ͨ͠ NavHost ( … ) { … composable("item/{id}") { val

    arguments = requireNotNull(it.arguments ) val itemId = ItemId(requireNotNull(arguments.getString("id")) ) ItemDetailScreen ( … ) } } navController.navigate("item/${item.id.value}") /BWJHBUJPO$PNQPTFͰ͸ભҠઌΛจࣈྻͰࢦఆ͢Δ Ͳ͔͜Ͱ ભҠઌͷจࣈྻΛͭ͘Δͱ͖ʹؒҧ͑ͦ͏ JEͷܕΛ੍ݶͰ͖ͳ͍
  6. fun NavHostController.navigateToItemDetail(id: ItemId) { navigate("item/${id.value}" ) } fun NavGraphBuilder.itemDetailNavGraph (

    navController: NavHostControlle r ) { composable("item/{id}") { val arguments = requireNotNull(it.arguments ) val itemId = ItemId(requireNotNull(arguments.getString("id")) ) ItemDetailScreen ( // .. . ) } } ભҠઌͷࢦఆΛUZQFTBGFʹ͍ͨ͠ /BW)PTUͱ͸ผͷϑΝΠϧͰ ભҠઌͷจࣈྻͷੜ੒ॲཧΛભҠઌͷఆٛͱಉ͡ͱ͜Ζʹॻ͘ ͜ͷϝιουܦ༝ͰભҠ͢ΔͷͰ ભҠઌͷจࣈྻΛؒҧ͑ͳ͍ JEͷܕΛ੍ݶͰ͖Δ
  7. ભҠઌͷࢦఆΛUZQFTBGFʹ͍ͨ͠ NavHost ( … ) { … itemDetailNavGraph(navController ) }

    Ͳ͔͜Ͱ navController.navigateToItemDetail(itemId) *UFN*Eܕ͔͠౉ͤͳ͍
  8. fun NavGraphBuilder.userNavGraph( navController: NavHostController ) { navigation( route = "user",

    startDestination = "user/top" ) { composable("user/top") { LaunchedEffect(Unit) { val isTutorialShown = … if (isTutorialShown) { navController.navigate("user/main") { popUpTo("user") } } else { navController.navigate("user/tutorial") { popUpTo("user") } } } } composable("user/tutorial") { … } composable("user/main") { … } OFTUFEHSBQI OBWJHBUF lVTFSz ͕ݺ͹ΕΔͱlVTFSUPQz൑ఆ༻ը໘͕ දࣔ͞ΕΔ ൑ఆ༻ը໘͔ΒνϡʔτϦΞϧը໘΍ Ϣʔβʔը໘ʹߦ͘ͱ͖ʹ QPQ6Q5P lVTFSz ͢Δ͜ͱͰ ൑ఆ༻ը໘͕όοΫελοΫ͔Β QPQ͞ΕΔ
  9. จࣈ৭͕൒ಁ໌ʹͳΒͳ͍Α͏ʹ͍ͨ͠ w എܠ্ͷจࣈ৭΍ΞΠίϯͷ৭͸গ͠ಁ໌ʹͳ͍ͬͯΔ MaterialTheme { Surface(color = MaterialTheme.colors.background) { //

    contentColor ͸ MaterialTheme.colors.onBackground (= Color.Black ) Column { // Color.Black.copy(alpha = 0.87f) ~= #21212 1 Text("Jetpack" ) // Color.Blac k Text(“Compose", color = MaterialTheme.colors.onBackground ) } } }
  10. UIFNF -PDBM$POUFOU$PMPS $POUFOU"MQIB IJHI NFEJVN EJTBCMFE MJHIU MVNJOBODF G G

    G MVNJOBODF G G G EBSL MVNJOBODF G G G MVNJOBODF G G G
  11. MaterialTheme { Surface(color = MaterialTheme.colors.background) { // contentColor ͸ MaterialTheme.colors.onBackground

    (= Color.Black ) Column { // Color.Black.copy(alpha = 0.87f) ~= #21212 1 Text("Jetpack" ) // Color.Blac k Text(“Compose", color = MaterialTheme.colors.onBackground ) } } } ྫʣMJHIUUIFNFͰDPOUFOU$PMPS͕ࠇʢ$PMPS#MBDLʣͷ৔߹ɺMVNJOBODF͸ҎԼ ˠ4VSGBDF಺ͷจࣈʢ5FYUʣ΍ΞΠίϯʢ*DPOʣ͸গ͠ʢGʣಁ໌ʹͳΔ
  12. MaterialTheme { CompositionLocalProvider ( LocalContentAlpha provides 1f , ) {

    Surface(color = MaterialTheme.colors.background) { Column { // Color.Blac k Text("Jetpack" ) // Color.Blac k Text("Compose", color = MaterialTheme.colors.onBackground ) } } } } $PNQPTJUJPO-PDBM1SPWJEFSͰ-PDBM$POUFOU"MQIBΛ্ॻ͖͢Δͱෆಁ໌ʹͰ͖Δ
  13. @Composabl e fun MyAppTheme(…) { MaterialTheme ( … ) {

    CompositionLocalProvider ( LocalContentAlpha provides 1f , ) { content( ) } } } ΞϓϦͷςʔϚʹ૊ΈࠐΉͷ΋͋Γ ͨͩ͠ɺಛఆͷ$PNQPTBCMF಺Ͱ -PDBM$POUFOU"MQIBʹ໌ࣔతʹ $POUFOU"MQIBIJHIͳͲΛࢦఆ͍ͯ͠Δ΋ ͷʢྫ5PQ"QQ#BSʣ͕͋ΔͷͰɺશͯͰ ஔ͖׵͑ΒΕΔΘ͚Ͱ͸ͳ͍ MyAppTheme { Surface(color = MaterialTheme.colors.background) { Column { // LocalContentAlpha ͸ 1 f Text("Hello" ) } } }
  14. 5PQ"QQ#BSͷBDUJPOTΛUJUMFͱಉ͡ಁ໌౓ʹ͍ͨ͠ @Composabl e fun TopAppBar ( … ) { AppBar

    ( … ) { … Row ( … ) { ProvideTextStyle(value = MaterialTheme.typography.h6) { CompositionLocalProvider ( LocalContentAlpha provides ContentAlpha.high , content = titl e ) } } CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { Row ( Modifier.fillMaxHeight() , horizontalArrangement = Arrangement.End , verticalAlignment = Alignment.CenterVertically , content = action s ) } OBWJHBUJPO*DPO΍UJUMF͸ $POUFOU"MQIBIJHI BDUJPOT͸ $POUFOU"MQIBNFEJVN
  15. 5PQ"QQ#BSͷBDUJPOTΛUJUMFͱಉ͡ಁ໌౓ʹ͍ͨ͠ TopAppBar ( title = { Text("AppBarSample") } , navigationIcon

    = { IconButton(onClick = { }) { Icon ( Icons.Default.ArrowBack , contentDescription = "back" , ) } } , actions = { CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) { IconButton(onClick = { }) { Icon ( Icons.Default.Share , contentDescription = "share" , ) } } } ) $POUFOU"MQIBIJHI ʹࠩ͠ସ͑Δ
  16. 3JQQMFͷ৭Λม͍͑ͨ w SFNFNCFS3JQQMFͰDPMPSΛࢦఆͨ͠΋ͷΛ .PEJ fi FSDMJDLBCMFͷJOEJDBUJPOʹࢦఆ͢Δ Modifier.clickable ( interactionSource =

    remember { MutableInteractionSource() } , indication = rememberRipple(color = MaterialTheme.colors.secondary ) ) { }
  17. w $PNQPTJUJPO-PDBM1SPWJEFSͰ-PDBM3JQQMF5IFNF Λࠩ͠ସ͑Δ 3JQQMFͷBMQIB΋ม͍͑ͨ private val MyRippleTheme = object :

    RippleTheme { @Composabl e override fun defaultColor(): Color { return MaterialTheme.colors.secondar y } @Composabl e override fun rippleAlpha(): RippleAlpha { return RippleAlpha(… ) } } @Composabl e fun Sample() { CompositionLocalProvider(LocalRippleTheme provides MyRippleTheme) { … } }
  18. 3JQQMFͷσϑΥϧτͷ৭ͱBMQIB UIFNF -PDBM$POUFOU$PMPS DPMPS BMQIB MJHIU MVNJOBODF -PDBM$POUFOU$PMPS -JHIU5IFNF)JHI$POUSBTU3JQQMF"MQIB MVNJOBODF

    -JHIU5IFNF-PX$POUSBTU3JQQMF"MQIB EBSL MVNJOBODF $PMPS8IJUF %BSL5IFNF3JQQMF"MQIB MVNJOBODF -PDBM$POUFOU$PMPS
  19. Լʹ͋Δཁૉ͕λοϓΛरΘͳ͍Α͏ʹ͍ͨ͠ w ॏͶ͚ͨͩͩͱλοϓ͸Լʹൈ͚Δ Box(contentAlignment = Alignment.Center) { Button(onClick = {

    /*TODO*/ }) { Text("Button" ) } Box ( modifier = Modifie r .size(200.dp ) .background(color = Color.Red.copy(alpha = 0.3f) ) ) { … } }
  20. Լʹ͋Δཁૉ͕λοϓΛरΘͳ͍Α͏ʹ͍ͨ͠ w .PEJ fi FSQPJOUFS*OQVU ͰλοϓΛर͏ Box(contentAlignment = Alignment.Center) {

    Button(onClick = { /*TODO*/ }) { Text("Button" ) } Box ( modifier = Modifie r .size(200.dp ) .background(color = Color.Red.copy(alpha = 0.3f) ) .pointerInput(Unit) { } ) { … } }
  21. Լʹ͋Δཁૉ͕λοϓΛरΘͳ͍Α͏ʹ͍ͨ͠ w .PEJ fi FSQPJOUFS*OQVU ͰλοϓΛर͏ w ·ͨ͸ɺ্ʹ͘ΔཁૉΛ4VSGBDFͰғΉ @Composabl e

    fun Surface ( … ) { Surface ( … clickAndSemanticsModifier = Modifie r .semantics(mergeDescendants = false) { } .pointerInput(Unit) { } ) }
  22. Լʹ͋Δཁૉ͕λοϓΛरΘͳ͍Α͏ʹ͍ͨ͠ w .PEJ fi FSQPJOUFS*OQVU ͰλοϓΛर͏ w ·ͨ͸ɺ্ʹ͘ΔཁૉΛ4VSGBDFͰғΉ Box(contentAlignment =

    Alignment.Center) { Button(onClick = { /*TODO*/ }) { Text("Button" ) } Surface ( color = Color.Red.copy(alpha = 0.3f) , modifier = Modifie r .size(200.dp ) ) { … } }
  23. ԣฒͼͷ5FYUΛCBTFMJOFͰἧ͍͑ͨ w 3PX4DPQFͷ.PEJ fi FSBMJHO#Z#BTFMJOF Λ࢖͏ Row { Text (

    text = "Hello" , modifier = Modifier.alignByBaseline( ) ) Text ( text = "Jetpack" , fontSize = 32.sp , modifier = Modifier.alignByBaseline( ) ) Text ( text = "Compose" , fontSize = 20.sp , modifier = Modifier.alignByBaseline( ) ) }
  24. 4FMFDUJPO$POUBJOFSͷબ୒࣌ͷ৭Λม͍͑ͨ w .BUFSJBM5IFNFͰDPMPSTͷQSJNBSZΛ্ॻ͖͢Δ val primary = MaterialTheme.colors.primar y val secondary

    = MaterialTheme.colors.secondar y MaterialTheme ( colors = MaterialTheme.colors.copy(primary = secondary ) ) { SelectionContainer { Text ( text = "Hello DroidKaigi" , fontSize = 32.sp , color = primar y ) } } બ୒จࣈͷCBDLHSPVOEDPMPSͷBMQIB஋Λ QSJNBSZͷ৭ʹԠ͍͍ͯ͡ײ͡ʹܭࢉͯ͘͠ΕΔ
  25. 4FMFDUJPO$POUBJOFSͷબ୒࣌ͷ৭Λม͍͑ͨ w -PDBM5FYU4FMFDUJPO$PMPSTΛࠩ͠ସ͑Δ val textSelectionColors = TextSelectionColors ( handleColor =

    MaterialTheme.colors.secondary , backgroundColor = MaterialTheme.colors.secondary.copy(alpha = 0.2f ) ) CompositionLocalProvider(LocalTextSelectionColors provides textSelectionColors) { SelectionContainer { Text ( text = "Hello DroidKaigi" , fontSize = 32.sp , color = MaterialTheme.colors.primar y ) } } બ୒จࣈͷCBDLHSPVOEDPMPSͷBMQIB஋΋ ࣗ෼Ͱௐ੔͢Δ
  26. 4QJOOFSͬͯͳ͍ͷʁˠ%SPQEPXO.FOV var selected by remember { mutableStateOf("not selected") } var

    expanded by remember { mutableStateOf(false) } OutlinedButton(onClick = { expanded = true }) { … } DropdownMenu ( expanded = expanded , onDismissRequest = { expanded = false } ) { DropdownMenuItem ( onClick = { selected = "Cupcake " expanded = fals e } ) { Text("Cupcake" ) } … }
  27. 0WFSGMPXNFOVͬͯͳ͍ͷʁˠ%SPQEPXO.FOV Scaffold ( topBar = { TopAppBar ( … ,

    actions = { var expanded by remember { mutableStateOf(false) } IconButton(onClick = { expanded = true }) { Icon ( Icons.Default.MoreVert , contentDescription = "more" , ) } DropdownMenu ( expanded = expanded , onDismissRequest = { expanded = false } ) { DropdownMenuItem(… ) … } } ) }
  28. ಠࣗσβΠϯͷ5FYU'JFMEΛ࡞Γ͍ͨ w #BTJD5FYU'JFMEΛ࢖͏ BasicTextField ( value = value , onValueChange

    = onValueChange , modifier = modifier , decorationBox = { Box ( modifier = Modifie r .background ( Color.LightGray , RoundedCornerShape(4.dp ) ) .padding(16.dp ) ) { it( ) } } )
  29. 4IBEPXʹ৭Λ͚͍ͭͨ w .PEJ fi FSTIBEPX Ͱ͸৭ΛࢦఆͰ͖ͳ͍ w 1BJOUBT'SBNFXPSL1BJOU ͱTFU4IBEPX-BZFS Λ࢖͏

    val modifier = Modifier.drawBehind { drawIntoCanvas { val paint = Paint( ) val frameworkPaint = paint.asFrameworkPaint( ) frameworkPaint.color = paintColo r frameworkPaint.setShadowLayer ( shadowRadius.toPx() , offsetX.toPx() , offsetY.toPx() , shadowColo r ) it.drawRect ( 0f, 0f, size.width, size.height, pain t ) } }
  30. "MFSU%JBMPHΛग़͍ͨ͠ var showAlertDialog by remember { mutableStateOf(false) } … if

    (showAlertDialog) { AlertDialog ( title = { Text("Dialog title") } , text = { Text("Dialog body text") } , confirmButton = { TextButton(onClick = { … }) { Text("Accept" ) } } , dismissButton = { TextButton(onClick = { showAlertDialog = false }) { Text("Cancel" ) } } , onDismissRequest = { showAlertDialog = false } ) }
  31. ೚ҙͷ%JBMPHΛग़͍ͨ͠ Dialog(onDismissRequest = { showDialog = false }) { Surface

    ( modifier = Modifier , shape = MaterialTheme.shapes.medium , color = MaterialTheme.colors.surface , ) { Column(modifier = Modifier.padding(vertical = 8.dp)) { Text ( text = "ΞϧόϜ͔Βબ୒" , modifier = … ) Text ( text = "ը૾Λ࡟আ" , modifier = … ) } } }
  32. %BUF1JDLFS5JNF1JDLFS%JBMPHΛग़͍ͨ͠ Button ( onClick = { DatePickerDialog ( context ,

    { _, year, monthOfYear, dayOfMonth - > … } , date.year , date.month - 1 , date.day , ) .show( ) } ) { Text(date.dateText ) } ৭͸5IFNFͷ DPMPS4FDPOEBSZ
  33. %BUF1JDLFS5JNF1JDLFS%JBMPHΛग़͍ͨ͠ @Composabl e fun DatePickerDialog ( onDismissRequest: () -> Unit

    , date: MyDate , onDateChange: (MyDate) -> Unit , ) { Dialog(onDismissRequest = onDismissRequest) { Surface(shape = MaterialTheme.shapes.medium) { Column { var editDate by remember { mutableStateOf(date) } AndroidView(factory = { DatePicker(it).apply { updateDate(editDate.year, … ) setOnDateChangedListener { … - > editDate = … } } }) … } } }
  34. .PEBM#PUUPN4IFFU-BZPVU ModalBottomSheetLayout ( sheetState = sheetState , sheetContent = {

    … } , modifier = Modifier.fillMaxSize( ) ) { Scaffold { … } } EJN͞ΕΔͷ͸DPOUFOUʹࢦఆͨ͠$PNQPTBCMF෦෼͚ͩ TUBUVT#BS΋EJN͢ΔͳΒ WindowCompat.setDecorFitsSystemWindows(window, false )
  35. TUBUVTCBS OBWJHBUJPOCBSͷαΠζΛ࢖͍͍ͨ w BDDPNQBOJTUͷJOTFUTΛ࢖͏ w HPPHMFHJUIVCJPBDDPNQBOJTUJOTFUT w QBEEJOH.PEJ fi FSͰऔಘ

    w .PEJ fi FSTZTUFN#BST1BEEJOH .PEJ fi FSTUBUVT#BST1BEEJOH  .PEJ fi FSOBWJHBUJPO#BST1BEEJOH  w IFJHIUͰऔಘ w .PEJ fi FSTUBUVT#BS)FJHIU .PEJ fi FSOBWJHBUJPO#BST)FJHIU .PEJ fi FSOBWJHBUJPO#BST8JEUI  w 1BEEJOH7BMVFTͰऔಘ w SFNFNCFS*OTFUT1BEEJOH7BMVFT rememberInsetsPaddingValues ( insets = LocalWindowInsets.current.navigationBars )
  36. $PMMBQTJOH5PPMCBS͕΄͍͠ʢ$PMVNOʣ Box(modifier = Modifier.fillMaxSize()) { val scrollState = rememberScrollState( )

    Column(modifier = Modifier.verticalScroll(scrollState)) { Spacer(modifier = Modifier.height(expandedHeight) ) … } Surface ( … modifier = Modifie r .height(expandedHeight ) .offset { val y = (-scrollState.value ) .coerceIn(-(expandedHeight - appBarHeight).roundToPx(), 0) IntOffset(0, y ) } ) { Box(modifier = Modifier.fillMaxSize()) { TopAppBar(…) } } }
  37. $PMMBQTJOH5PPMCBS͕΄͍͠ʢ-B[Z$PMVNOʣ Box(modifier = Modifier.fillMaxSize()) { val listState = rememberLazyListState( )

    LazyColumn(state = listState) { item { Spacer(modifier = Modifier.height(expandedHeight) ) } … } Surface ( … modifier = Modifie r .height(expandedHeight ) .offset { val scrollValue = if (listState.firstVisibleItemIndex == 0) { listState.firstVisibleItemScrollOffse t } else { Int.MAX_VALU E } val y = (-scrollValue ) .coerceIn(-(expandedHeight - appBarHeight).roundToPx(), 0 ) IntOffset(0, y ) } {
  38. ԼʹεΫϩʔϧͨ͠ͱ͖ʹग़ͯ͘Δ5PPMCBS͕΄͍͠ val toolbarOffsetY = remember { mutableStateOf(0f) } val nestedScrollConnection

    = remember { object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { val newOffset = toolbarOffsetY.value + available.y toolbarOffsetY.value = newOffset.coerceIn(-toolbarHeightPx, 0f ) return Offset.Zer o } } } Box(modifier = Modifier.fillMaxSize().nestedScroll(nestedScrollConnection)) { LazyColumn(contentPadding = PaddingValues(top = toolbarHeight)) { … } TopAppBar ( title = { Text("NestedScrollConnectionSample") } , modifier = Modifie r .height(toolbarHeight ) .offset { IntOffset(x = 0, y = toolbarOffsetY.value.roundToInt()) } ) }
  39. ԼʹεΫϩʔϧͨ͠ͱ͖ʹग़ͯ͘Δ5PPMCBS͕΄͍͠ val toolbarOffsetY = remember { mutableStateOf(0f) } val nestedScrollConnection

    = remember { object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { val newOffset = toolbarOffsetY.value + available.y toolbarOffsetY.value = newOffset.coerceIn(-toolbarHeightPx, 0f ) return available } } } Box(modifier = Modifier.fillMaxSize().nestedScroll(nestedScrollConnection)) { LazyColumn(contentPadding = PaddingValues(top = toolbarHeight)) { … } TopAppBar ( title = { Text("NestedScrollConnectionSample") } , modifier = Modifie r .height(toolbarHeight ) .offset { IntOffset(x = 0, y = toolbarOffsetY.value.roundToInt()) } ) } εΫϩʔϧΛ͜͜ͰফඅͰ͖Δ
  40. GMFYCPYMBZPVU͕΄͍͠ w ຊମʹੲ͋ͬͨ'MPX$PMVNOͱ'MPX3PX͸EFQSFDBUFEʹͳͬͨ w BDDPNQBOJTUͷ fl PXMBZPVUΛ࢖͏ w HPPHMFHJUIVCJPBDDPNQBOJTU fl

    PXMBZPVU FlowRow ( mainAxisSpacing = 8.dp , modifier = Modifier.fillMaxWidth( ) ) { repeat(10) { Button(onClick = { /*TODO*/ }) { Text("Button $it" ) } } }
  41. ը໘දࣔ࣌ʹΩʔϘʔυΛग़͍ͨ͠ w 'PDVT3FRVFTUFSΛ࢖͏ val focusRequester = remember { FocusRequester() }

    TextField ( … modifier = Modifie r .focusOrder(focusRequester) // or .focusRequester(focusRequester ) ) LaunchedEffect(Unit) { focusRequester.requestFocus( ) }
  42. ΩʔϘʔυΛ։ด͍ͨ͠ʢࠓ·Ͱʣ w 5FYU*OQVU4FSWJDFΛ࢖͏ w 5FYU'JFMEͳͲςΩετೖྗΛड͚औΕΔDPNQPTBCMF ͕ϑΥʔΧε͞Ε͍ͯͳ͍ͱΩʔϘʔυ͸ग़ͳ͍ val textInputService = LocalTextInputService.curren

    t Button(onClick = { textInputService?.showSoftwareKeyboard( ) }) { … } Button(onClick = { textInputService?.hideSoftwareKeyboard( ) }) { … } !*OUFSOBM5FYU"1*͕͍ͭͯ %FQSFDBUFEʹͳΔ༧ఆ IUUQTJTTVFUSBDLFSHPPHMFDPNJTTVFT
  43. ΤϯλʔΩʔ͕ԡ͞Εͨͱ͖ʹϑΥʔΧεҠಈ͍ͨ͠ val focusRequester1 = remember { FocusRequester() } val focusRequester2

    = remember { FocusRequester() } val focusManager = LocalFocusManager.curren t TextField ( … singleLine = true , keyboardActions = KeyboardActions { focusManager.moveFocus(FocusDirection.Next ) } , modifier = Modifie r .focusOrder(focusRequester1) { next = focusRequester 2 down = focusRequester 2 } , ) TextField ( … modifier = Modifie r .focusOrder(focusRequester2 ) )
  44. PO3FTVNFͷλΠϛϯάͰॲཧΛ͍ͨ͠ @Composabl e fun DoOnResume(action: () -> Unit) { val

    currentAction by rememberUpdatedState(action ) val lifecycle = LocalLifecycleOwner.current.lifecycl e val lifecycleObserver = remember { LifecycleEventObserver { _, event - > if (event == Lifecycle.Event.ON_RESUME) { currentAction( ) } } } DisposableEffect(lifecycle, lifecycleObserver) { lifecycle.addObserver(lifecycleObserver ) onDispose { lifecycle.removeObserver(lifecycleObserver ) } } }
  45. PO3FTVNFͷλΠϛϯάͰॲཧΛ͍ͨ͠ @Composabl e fun DoOnResume(action: () -> Unit) { val

    currentAction by rememberUpdatedState(action ) val lifecycle = LocalLifecycleOwner.current.lifecycl e val lifecycleObserver = remember { LifecycleEventObserver { _, event - > if (event == Lifecycle.Event.ON_RESUME) { currentAction( ) } } } DisposableEffect(lifecycle, lifecycleObserver) { lifecycle.addObserver(lifecycleObserver ) onDispose { lifecycle.removeObserver(lifecycleObserver ) } } }
  46. $BNFSB9Λ࢖͍͍ͨ w "OESPJE7JFXDPNQPTBCMFΛ࢖ͬͯ1SFWJFX7JFXΛ૊ΈࠐΉ w 1SFWJFX7JFXͷJNQMFNFOUBUJPO.PEFͰ$0.1"5*#-&Λࢦఆ͢Δ AndroidView ( modifier = Modifier.fillMaxSize()

    , factory = { context - > PreviewView(context).apply { implementationMode = PreviewView.ImplementationMode.COMPATIBL E … } } , update = { … } )
  47. 7JFX.PEFMͷίϯετϥΫλͰJEΛ౉͍ͨ͠ w %BHHFSͷ"TTJTUFE*OKFDUػೳΛ࢖͏ IUUQZBO[NCMPHTQPUDPNKFUQBDLDPNQPTFWJFXNPEFMOBWJHBUJPOIUNM class ItemDetailViewModel @AssistedInject constructor ( @Assisted

    val id: ItemId , … , ) : ViewModel() { @AssistedFactor y interface Factory { fun create(id: ItemId): ItemDetailViewMode l } @EntryPoin t @InstallIn(ActivityComponent::class ) interface ActivityCreatorEntryPoint { fun getItemDetailViewModelFactory(): Factor y } companion object { {
  48. .VUBCMF4UBUFͷঢ়ଶΛ4BWFE4UBUF)BOEMFʹอଘ͍ͨ͠ w 4BWFE4UBUF)BOEMFTFU4BWFE4UBUF1SPWJEFS Λ࢖͏ͱศར @HiltViewMode l class MyViewModel @Inject constructor

    ( … savedStateHandle: SavedStateHandl e ) : ViewModel() { private val _uiState = mutableStateOf<UiState>(UiState.Initial ) … init { savedStateHandle.setSavedStateProvider("[KEY]") { _uiState.value.toBundle( ) } savedStateHandle.get<Bundle>("[KEY]")?.let { _uiState.value = it.toUiState( ) }