Save 37% off PRO during our Black Friday Sale! »

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

D2bcabeeb1ddff142fb8988b412cb4d3?s=47 Yuki Anzai
October 19, 2021

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

DroidKaigi 2021

D2bcabeeb1ddff142fb8988b412cb4d3?s=128

Yuki Anzai

October 19, 2021
Tweet

Transcript

  1. :VLJ"O[BJ !ZBO[N  %SPJE,BJHJ ϓϩμΫτϨϕϧͰඞཁʹͳΔ +FUQBDL$PNQPTFςΫχοΫ

  2. :VLJ"O[BJ w (PPHMF%FWFMPQFS&YQFSUGPS"OESPJE w UXJUUFS!ZBO[N w CMPHZBO[NCMPHTQPUDPN w גࣜձࣾ΢ϑΟΧ

  3. ͔ΒGVMM+FUQBDL$PNQPTFͰ։ൃ IUUQTCKQ

  4. ໨࣍ w ൚༻తͳςΫχοΫ w ը໘ભҠΛͲ͏͢Δ͔/BWJHBUJPO$PNQPTFͱ7JFX.PEFM w ཉ͍͠$PNQPTBCMF΍ػೳͷ୳͠ํ w ࡉ͔͍5JQT w

    ͍Ζ͍Ζ঺հ͠·͢
  5. ը໘ભҠΛͲ͏͢Δ͔

  6. ը໘ભҠΛͲ͏͢Δʁ ը໘ભҠ DPNQPTFTBNQMFT ෳ਺"DUJWJUZ "DUJWJUZભҠ 'SBHNFOUࠩ͠ସ͑ $SBOF "DUJWJUZ 'SBHNFOUࠩ͠ସ͑ +FUDIBU

    +FUTVSWFZ "DUJWJUZ $PNQPTF +FU/FXT +FUDBTUFS 3BMMZ 0XM
  7. ը໘ભҠͲ͏͢Δʁ ը໘ભҠ DPNQPTFTBNQMFT ෳ਺"DUJWJUZ "DUJWJUZભҠ 'SBHNFOUࠩ͠ସ͑ $SBOF "DUJWJUZ 'SBHNFOUࠩ͠ସ͑ +FUDIBU

    +FUTVSWFZ "DUJWJUZ $PNQPTF +FU/FXT +FUDBTUFS 3BMMZ 0XM
  8. ը໘ભҠͲ͏͢Δʁ ը໘ભҠ DPNQPTFTBNQMFT ෳ਺"DUJWJUZ "DUJWJUZભҠ 'SBHNFOUࠩ͠ସ͑ $SBOF "DUJWJUZ 'SBHNFOUࠩ͠ସ͑ +FUDIBU

    +FUTVSWFZ "DUJWJUZ $PNQPTF /BWJHBUJPO$PNQPTF +FU/FXT +FUDBTUFS 3BMMZ 0XM ͦͷଞ
  9. ը໘ભҠͲ͏͢Δʁ ը໘ભҠ DPNQPTFTBNQMFT ෳ਺"DUJWJUZ "DUJWJUZભҠ 'SBHNFOUࠩ͠ସ͑ $SBOF "DUJWJUZ 'SBHNFOUࠩ͠ସ͑ +FUDIBU

    +FUTVSWFZ "DUJWJUZ $PNQPTF /BWJHBUJPO$PNQPTF +FU/FXT +FUDBTUFS 3BMMZ 0XM ͦͷଞ
  10. "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() ) { … }
  11. /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( … ) } }
  12. ཉ͍͠$PNQPTBCMF΍ػೳͷ ୳͠ํ

  13. SFGFSFODFΛΈΔ w BOESPJEYDPNQPTF ͷ5PQMFWFM GVODUJPOTTVNNBSZ w BOESPJEYDPNQPTFNBUFSJBMͷ $PNQPOFOUT IUUQTEFWFMPQFSBOESPJEDPNSFGFSFODFLPUMJOBOESPJEYDPNQPTFNBUFSJBMQBDLBHFTVNNBSZDPNQPOFOUT

  14. BDDPNQBOJTUʹͳ͍͔ௐ΂Δ w IUUQTHJUIVCDPNHPPHMFBDDPNQBOJTU w +FUQBDL$PNQPTFͷศརϥΠϒϥϦू w 4XJQFUP3FGSFTI w 1BHFS w

    ʜ
  15. ,PUMJO4MBDLͷDPNQPTFνϟϯωϧͰݕࡧ͢Δ w IUUQTLPUMJOMBOHTMBDLDPN w IUUQTLPUMJOMBOHPSHDPNNVOJUZ͔Βট଴ΛϦΫΤετͰ͖·͢ ΩʔϘʔυ͕දࣔ͞Εͨͱ͖ʹɺϑΥʔΧε͞ Ε͍ͯΔ5FYU'JFME͕ΩʔϘʔυͷԼʹӅΕͳ ͍Α͏ʹࣗಈͰεΫϩʔϧ͢Δํ๏͕͋Δ͔ʁ ˣ TDSPMMGPDVTͰݕࡧ

    ˣ 3FMPDBUJPO3FRVFTUFSͱ͍͏ͷ͕͋ΔΒ͍͠
  16. $PNQPTFͷαϯϓϧίʔυΛಡΉ w $PNQPTFͷػೳ͸αϯϓϧ͕༻ҙ͞Ε͍ͯΔ͜ͱ͕ଟ͍ w ,PUMJOTMBDL΍TUBDLPWFS fl PXͰݟ͚ͭͨػೳͷαϯϓϧ͕ͳ͍͔ "OESPJE$PEF4FBSDIͰௐ΂Δ w IUUQTDTBOESPJEDPNBOESPJEYQMBUGPSNGSBNFXPSLTTVQQPSU

     BOESPJEYNBJODPNQPTF w
  17. None
  18. +FUQBDL$PNQPTF3PBENBQΛνΣοΫ͢Δ w ࠓޙͷ࣮૷༧ఆ͕ެ։͞Ε͍ͯΔ w ࣗ෼Ͱ࣮૷͢Δ͔ɺ༻ҙ͞ΕΔ·Ͱ଴͔ͭͷ൑அࡐྉʹͳΔ IUUQTEFWFMPQFSBOESPJEDPNKFUQBDLBOESPJEYDPNQPTFSPBENBQ

  19. ࣗ෼Ͱ࣮૷͢ΔSEQBSUZMJCSBSZΛ୳͢ w ࣗ෼Ͱ࣮૷͢Δ w άϥϑͱ͔ˠ$BOWBTDPNQPTBCMF .PEJ fi FSESBX99  w

    ಠࣗͷαΠζɾ഑ஔˠ-BZPVUDPNQPTBCMF .PEJ fi FSMBZPVU  w SEQBSUZMJCSBSZ w ྫʣඇಉظը૾ಡΈࠐΈˠDPJM
  20. /BWJHBUJPO$PNQPTFؔ܎ͷ ςΫχοΫ

  21. ભҠઌͷࢦఆΛ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Ͱ͸ભҠઌΛจࣈྻͰࢦఆ͢Δ Ͳ͔͜Ͱ
  22. ભҠઌͷࢦఆΛ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ͷܕΛ੍ݶͰ͖ͳ͍
  23. 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ͷܕΛ੍ݶͰ͖Δ
  24. ભҠઌͷࢦఆΛUZQFTBGFʹ͍ͨ͠ NavHost ( … ) { … itemDetailNavGraph(navController ) }

    Ͳ͔͜Ͱ navController.navigateToItemDetail(itemId) *UFN*Eܕ͔͠౉ͤͳ͍
  25. ॳճ͚ͩνϡʔτϦΞϧը໘Λग़͍ͨ͠ OBWJHBUF5P6TFS νϡʔτϦΞϧ ະදࣔ νϡʔτϦΞϧදࣔࡁΈ νϡʔτϦΞϧ ը໘ Ϣʔβʔ ը໘

  26. ॳճ͚ͩνϡʔτϦΞϧը໘Λग़͍ͨ͠ OBWJHBUF5P6TFS νϡʔτϦΞϧ ະදࣔ νϡʔτϦΞϧදࣔࡁΈ લͷը໘ લͷը໘ νϡʔτϦΞϧ ը໘ લͷը໘

    Ϣʔβʔ ը໘
  27. ॳճ͚ͩνϡʔτϦΞϧը໘Λग़͍ͨ͠ OBWJHBUF5P6TFS νϡʔτϦΞϧ ະදࣔ νϡʔτϦΞϧදࣔࡁΈ લͷը໘ લͷը໘ νϡʔτϦΞϧ ը໘ લͷը໘

    Ϣʔβʔ ը໘ ͜͜ΛͲ͏͢Δ͔
  28. ॳճ͚ͩνϡʔτϦΞϧը໘Λग़͍ͨ͠ OBWJHBUF5P6TFS νϡʔτϦΞϧ ະදࣔ νϡʔτϦΞϧදࣔࡁΈ લͷը໘ લͷը໘ νϡʔτϦΞϧ ը໘ લͷը໘

    Ϣʔβʔ ը໘ લͷը໘ ൑ఆ༻ը໘
  29. ॳճ͚ͩνϡʔτϦΞϧը໘Λग़͍ͨ͠ OBWJHBUF5P6TFS νϡʔτϦΞϧ ະදࣔ νϡʔτϦΞϧදࣔࡁΈ લͷը໘ νϡʔτϦΞϧ ը໘ Ϣʔβʔ ը໘

    લͷը໘ ൑ఆ༻ը໘ OFTUFEHSBQI
  30. 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͞ΕΔ
  31. ެࣜυΩϡϝϯτʹ ʢ͋·Γʣॻ͍ͯͳ͍5JQT

  32. .BUFSJBM5IFNFؔ܎

  33. จࣈ৭͕൒ಁ໌ʹͳΒͳ͍Α͏ʹ͍ͨ͠ 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 ) } } }
  34. -PDBM$POUFOU"MQIB͕ద༻͞Ε͍ͯΔ w .BUFSJBM5IFNFͰ-PDBM$POUFOU"MQIBʹ$POUFOU"MQIBIJHI͕ηοτ͞ Ε͍ͯΔ

  35. -PDBM$POUFOU"MQIB͕ద༻͞Ε͍ͯΔ w .BUFSJBM5IFNFͰ-PDBM$POUFOU"MQIBʹ$POUFOU"MQIBIJHI͕ηοτ͞ Ε͍ͯΔ w $POUFOU"MQIBʹ͸IJHI NFEJVN EJTBCMFE͕͋Δ w $POUFOU"MQIBͷ֤஋͸-PDBM$POUFOU$PMPSͷMVNJOBODFʢ໌Δ͞ʣͱ

    UIFNF MJHIUPSEBSL Ͱܾ·Δ
  36. 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
  37. 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ʣಁ໌ʹͳΔ
  38. 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Λ্ॻ͖͢Δͱෆಁ໌ʹͰ͖Δ
  39. @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" ) } } }
  40. 5PQ"QQ#BSͷBDUJPOTΛUJUMFͱಉ͡ಁ໌౓ʹ͍ͨ͠ OBWJHBUJPO*DPO΍UJUMFʹൺ΂ͯബ͍ ͳΜͰʁ

  41. 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
  42. 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 ʹࠩ͠ସ͑Δ
  43. 3JQQMFؔ܎

  44. TFMFDUBCMF*UFN#BDLHSPVOE#PSEFSMFTT w SFNFNCFS3JQQMFͰCPVOEFEʹGBMTFΛࢦఆͨ͠ ΋ͷΛ.PEJ fi FSDMJDLBCMFͷJOEJDBUJPOʹࢦఆ͢Δ Modifier.clickable ( interactionSource =

    remember { MutableInteractionSource() } , indication = rememberRipple(bounded = false, radius = 40.dp ) ) { }
  45. 3JQQMFͷ৭Λม͍͑ͨ w SFNFNCFS3JQQMFͰDPMPSΛࢦఆͨ͠΋ͷΛ .PEJ fi FSDMJDLBCMFͷJOEJDBUJPOʹࢦఆ͢Δ Modifier.clickable ( interactionSource =

    remember { MutableInteractionSource() } , indication = rememberRipple(color = MaterialTheme.colors.secondary ) ) { }
  46. 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) { … } }
  47. 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
  48. $PNQPTBCMFؔ܎

  49. Լʹ͋Δཁૉ͕λοϓΛरΘͳ͍Α͏ʹ͍ͨ͠ 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) ) ) { … } }
  50. Լʹ͋Δཁૉ͕λοϓΛरΘͳ͍Α͏ʹ͍ͨ͠ 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) { } ) { … } }
  51. Լʹ͋Δཁૉ͕λοϓΛरΘͳ͍Α͏ʹ͍ͨ͠ w .PEJ fi FSQPJOUFS*OQVU ͰλοϓΛर͏ w ·ͨ͸ɺ্ʹ͘ΔཁૉΛ4VSGBDFͰғΉ @Composabl e

    fun Surface ( … ) { Surface ( … clickAndSemanticsModifier = Modifie r .semantics(mergeDescendants = false) { } .pointerInput(Unit) { } ) }
  52. Լʹ͋Δཁૉ͕λοϓΛरΘͳ͍Α͏ʹ͍ͨ͠ 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 ) ) { … } }
  53. ԣฒͼͷ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( ) ) }
  54. 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ͷ৭ʹԠ͍͍ͯ͡ײ͡ʹܭࢉͯ͘͠ΕΔ
  55. 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஋΋ ࣗ෼Ͱௐ੔͢Δ
  56. 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" ) } … }
  57. 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(… ) … } } ) }
  58. ಠࣗσβΠϯͷ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( ) } } )
  59. 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 ) } }
  60. %JBMPHؔ܎

  61. "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 } ) }
  62. ೚ҙͷ%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 = … ) } } }
  63. %BUF1JDLFS5JNF1JDLFS%JBMPHΛग़͍ͨ͠ Button ( onClick = { DatePickerDialog ( context ,

    { _, year, monthOfYear, dayOfMonth - > … } , date.year , date.month - 1 , date.day , ) .show( ) } ) { Text(date.dateText ) } ৭͸5IFNFͷ DPMPS4FDPOEBSZ
  64. %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 = … } } }) … } } }
  65. ϨΠΞ΢τؔ܎

  66. #PUUPN4IFFUΛग़͍ͨ͠ w NBUFSJBMJPDPNQPOFOUTTIFFUTCPUUPNͷछྨʹରԠͨ͠$PNQPTBCMF w 4UBOEBSECPUUPNTIFFUˠ#PUUPN4IFFU4DB ff PME w .PEBMCPUUPNTIFFUˠ.PEBM#PUUPN4IFFU-BZPVU w

    #PUUPN%SBXFS͸#PUUPN"QQ#BSͰ࢖͏ w NBUFSJBMJPDPNQPOFOUTOBWJHBUJPOESBXFSCPUUPNESBXFS
  67. .PEBM#PUUPN4IFFU-BZPVU ModalBottomSheetLayout ( sheetState = sheetState , sheetContent = {

    … } , modifier = Modifier.fillMaxSize( ) ) { Scaffold { … } } EJN͞ΕΔͷ͸DPOUFOUʹࢦఆͨ͠$PNQPTBCMF෦෼͚ͩ TUBUVT#BS΋EJN͢ΔͳΒ WindowCompat.setDecorFitsSystemWindows(window, false )
  68. 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 )
  69. $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(…) } } }
  70. $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 ) } {
  71. ԼʹεΫϩʔϧͨ͠ͱ͖ʹग़ͯ͘Δ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()) } ) }
  72. ԼʹεΫϩʔϧͨ͠ͱ͖ʹग़ͯ͘Δ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()) } ) } εΫϩʔϧΛ͜͜ͰফඅͰ͖Δ
  73. 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" ) } } }
  74. ͦͷଞ

  75. ը໘දࣔ࣌ʹΩʔϘʔυΛग़͍ͨ͠ w 'PDVT3FRVFTUFSΛ࢖͏ val focusRequester = remember { FocusRequester() }

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

    t Button(onClick = { textInputService?.showSoftwareKeyboard( ) }) { … } Button(onClick = { textInputService?.hideSoftwareKeyboard( ) }) { … } !*OUFSOBM5FYU"1*͕͍ͭͯ %FQSFDBUFEʹͳΔ༧ఆ IUUQTJTTVFUSBDLFSHPPHMFDPNJTTVFT
  77. ΩʔϘʔυΛ։ด͍ͨ͠ʢ͜Ε͔Βʣ w -PDBM4PGUXBSF,FZCPBSE$POUSPMMFSΛ࢖͏ w 5FYU'JFMEͳͲςΩετೖྗΛड͚औΕΔDPNQPTBCMF ͕ϑΥʔΧε͞Ε͍ͯͳ͍ͱΩʔϘʔυ͸ग़ͳ͍ val keyboardController = LocalSoftwareKeyboardController.curren

    t Button(onClick = { keyboardController?.show( ) }) { … } Button(onClick = { keyboardController?.hide( ) }) { … }
  78. ΤϯλʔΩʔ͕ԡ͞Εͨͱ͖ʹϑΥʔΧεҠಈ͍ͨ͠ 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 ) )
  79. 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 ) } } }
  80. 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 ) } } }
  81. $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 = { … } )
  82. 7JFX.PEFMؔ܎

  83. 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 { {
  84. .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( ) }
  85. ·ͱΊ w ը໘ભҠ΋+FUQBDL$PNQPTFͰ΍Δ৔߹ɺ/BWJHBUJPO$PNQPTFΛ࢖͏ ͷ͕͓͢͢Ί w "OESPJE$PEF4FBSDI DTBOESPJEDPN Ͱ$PNQPTBCMFͷ࣮૷ίʔυ΍ αϯϓϧίʔυΛಡ΋͏ w

    +FUQBDL$PNQPTF࢖ͬͯΈΑ͏ʂ
  86. ͓ΘΓ