Slide 1

Slide 1 text

:VLJ"O[BJ !ZBO[N *0FYUFOEFE /PXJO"OESPJEΞϓϦղઆ

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

/PXJO"OESPJEΞϓϦ w IUUQTHJUIVCDPNBOESPJEOPXJOBOESPJE w ."%ʢ.PEFSO"OESPJE%FWFMPQNFOUʣͳαϯϓϧΞϓϦ w ,PUMJO+FUQBDL$PNQPTF

Slide 4

Slide 4 text

/PXJO"OESPJE͸<8PSLJOQSPHSFTT🚧>Ͱ͋Γɺ ͜͜Ͱ঺հͨ͠಺༰ʢ೥݄೔࣌఺ʣ͕ ࠓޙมߋ͞ΕΔՄೳੑ͕͋Γ·͢

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

BOESPJEBQQMJDBUJPO

Slide 7

Slide 7 text

BOESPJEBQQMJDBUJPO NBDSPCFODINBSL

Slide 8

Slide 8 text

BOESPJEBQQMJDBUJPO NBDSPCFODINBSL BOESPJEMJCSBSZ

Slide 9

Slide 9 text

BOESPJEBQQMJDBUJPO NBDSPCFODINBSL BOESPJEMJCSBSZ ʁ

Slide 10

Slide 10 text

ʁ pluginManagement { includeBuild("build-logic") repositories { google() mavenCentral() gradlePluginPortal() } } …

Slide 11

Slide 11 text

pluginManagement { includeBuild("build-logic") repositories { google() mavenCentral() gradlePluginPortal() } } … QMVHJOCVJMEͷ SPPUQSPKFDUEJSFDUPSZ ಠࣗQMVHJOͷQSPKFDU

Slide 12

Slide 12 text

NPEVMF

Slide 13

Slide 13 text

No content

Slide 14

Slide 14 text

plugins { `kotlin-dsl` } group = "com.google.samples.apps.nowinandroid.buildlogic" java { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } dependencies { implementation(libs.android.gradlePlugin) implementation(libs.kotlin.gradlePlugin) implementation(libs.spotless.gradlePlugin) } gradlePlugin { plugins { … } } ʁ implementation("com.android.tools.build:gradle:7.2.1") ͷΑ͏ͳจࣈྻࢦఆͰ͸ͳ͍ MJCTBESPJEHSBEMF1MVHJO͸Ͳ͜Λࢦ͍ͯ͠Δʁ

Slide 15

Slide 15 text

ϥΠϒϥϦ؅ཧ w HSBEMFͷ7FSTJPO$BUBMPH w IUUQTEPDTHSBEMFPSHDVSSFOUVTFSHVJEFQMBUGPSNTIUNM w TFUUJOHTHSBEMF಺ʹ௚઀ఆٛPS50.-ϑΝΠϧʹఆٛ

Slide 16

Slide 16 text

ϥΠϒϥϦ؅ཧ w HSBEMFͷ7FSTJPO$BUBMPH w IUUQTEPDTHSBEMFPSHDVSSFOUVTFSHVJEFQMBUGPSNTIUNM dependencyResolutionManagement { … versionCatalogs { create("libs") { from(files("../gradle/libs.versions.toml")) } } } …

Slide 17

Slide 17 text

[versions] accompanist = "0.24.8-beta" androidDesugarJdkLibs = "1.1.5" androidGradlePlugin = "7.2.1" androidxActivity = "1.4.0" androidxAppCompat = "1.3.0" androidxCompose = "1.2.0-beta02" … [libraries] accompanist-flowlayout = { group = "com.google.accompanist", name = "accompanist-flowlayout", version.ref = "accompanist" } android-desugarJdkLibs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "androidDesugarJdkLibs" } android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidxActivity" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppCompat" } … [plugins] protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }

Slide 18

Slide 18 text

[versions] accompanist = "0.24.8-beta" androidDesugarJdkLibs = "1.1.5" androidGradlePlugin = "7.2.1" androidxActivity = "1.4.0" androidxAppCompat = "1.3.0" androidxCompose = "1.2.0-beta02" … [libraries] accompanist-flowlayout = { group = "com.google.accompanist", name = "accompanist-flowlayout", version.ref = "accompanist" } android-desugarJdkLibs = { group = "com.android.tools", name = "desugar_jdk_libs", version.ref = "androidDesugarJdkLibs" } android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidxActivity" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppCompat" } … [plugins] protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }

Slide 19

Slide 19 text

… dependencies { implementation(libs.android.gradlePlugin) … } dependencyResolutionManagement { … versionCatalogs { create("libs") { from(files("...")) } } } … … [libraries] … android-gradlePlugin = { … } …

Slide 20

Slide 20 text

dependencies { … implementation(libs.androidx.activity.compose) implementation(libs.androidx.appcompat) … dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { … } } WFSTJPO$BUBMPHT͕ࢦఆ͞Ε͍ͯͳ͍ɺ͜ͷMJCT͸Ͳ͔͜Βʁ

Slide 21

Slide 21 text

ϥΠϒϥϦ؅ཧ w HSBEMFͷ7FSTJPO$BUBMPH w IUUQTEPDTHSBEMFPSHDVSSFOUVTFSHVJEFQMBUGPSNTIUNM w TFUUJOHTHSBEMF಺ʹ௚઀ఆٛPS50.-ϑΝΠϧʹఆٛ w HSBEMFͷԼʹMJCTWFSTJPOTUPNMͱ͍͏໊લͰϑΝΠϧΛ༻ҙ͢Δͱɺ MJCTͱ͍͏໊લͰ7FSTJPO$BUBMPH͕࡞ΒΕΔ

Slide 22

Slide 22 text

CVJMEMPHJDͷDVTUPNHSBEMFQMVHJO͸ԿΛ͍ͯ͠Δͷ͔ʁ

Slide 23

Slide 23 text

CVJMEMPHJDͷDVTUPNHSBEMFQMVHJO͸ԿΛ͍ͯ͠Δͷ͔ʁ plugins { id("nowinandroid.android.application") id("nowinandroid.android.application.compose") id("nowinandroid.android.application.jacoco") kotlin("kapt") id("jacoco") id("dagger.hilt.android.plugin") id("nowinandroid.spotless") } android { … } dependencies { … } ʁ OPXJOBOESPJE99ͷJEΛࢦఆ͍ͯͯ͠ id("com.android.application") Ͱ͸ͳ͍

Slide 24

Slide 24 text

… gradlePlugin { plugins { register("androidApplicationCompose") { id = "nowinandroid.android.application.compose" implementationClass = "AndroidApplicationComposeConventionPlugin" } register("androidApplication") { id = "nowinandroid.android.application" implementationClass = "AndroidApplicationConventionPlugin" } … } }

Slide 25

Slide 25 text

class AndroidApplicationConventionPlugin : Plugin { override fun apply(target: Project) { with(target) { with(pluginManager) { apply("com.android.application") apply("org.jetbrains.kotlin.android") } extensions.configure { configureKotlinAndroid(this) defaultConfig.targetSdk = 32 } } } } ͜͜Ͱࢦఆ͞Ε͍ͯΔ BOESPJECMPDLͰͷઃఆͷڞ௨Խ

Slide 26

Slide 26 text

internal fun Project.configureKotlinAndroid( commonExtension: CommonExtension<*, *, *, *>, ) { commonExtension.apply { compileSdk = 32 defaultConfig { minSdk = 21 } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 isCoreLibraryDesugaringEnabled = true } kotlinOptions { … } } … }

Slide 27

Slide 27 text

ΞϓϦͷߏ੒ w BOESPJEBQQMJDBUJPONPEVMF w BQQϝΠϯͷΞϓϦ w BQQOJBDBUBMPHΞϓϦ༻ͷ6*DPNQPOFOUTΛݟΕΔΧλϩάΞϓϦ

Slide 28

Slide 28 text

BQQOJBDBUBMPH app-nia-catalog core-ui core-model

Slide 29

Slide 29 text

BQQOJBDBUBMPH app-nia-catalog core-ui core-model data class Author( val id: String, val name: String, val imageUrl: String, val twitter: String, val mediumPage: String, val bio: String, )

Slide 30

Slide 30 text

BQQOJBDBUBMPH app-nia-catalog core-ui core-model @Composable fun NiaFilledButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, small: Boolean = false, … content: @Composable RowScope.() -> Unit ) { Button( … ) }

Slide 31

Slide 31 text

BQQOJBDBUBMPH app-nia-catalog core-model core-ui

Slide 32

Slide 32 text

BQQ

Slide 33

Slide 33 text

App core-ui core-navigation feature-author sync feature-interests feature-foryou feature-topic core-model core-common core-data core- datastore core- database core- network ˎUFTU༻ͷNPEVMF͸ আ͍͍ͯ·͢

Slide 34

Slide 34 text

App core-ui core-navigation feature-author sync feature-interests feature-foryou feature-topic core-model core-common core-data core- datastore core- database core- network ˎUFTU༻ͷNPEVMF͸ আ͍͍ͯ·͢

Slide 35

Slide 35 text

App core-ui core-navigation feature-author sync feature-interests feature-foryou feature-topic core-model core-common core-data core- datastore core- database core- network ˎUFTU༻ͷNPEVMF͸ আ͍͍ͯ·͢ w *0༻ͷ$PSPVUJOF%JTQBUDIFS w TFBMFEJOUFSGBDF3FTVMUPVU5

Slide 36

Slide 36 text

App core-ui core-navigation feature-author sync feature-interests feature-foryou feature-topic core-model core-common core-data core- datastore core- database core- network ˎUFTU༻ͷNPEVMF͸ আ͍͍ͯ·͢ 1SPUPEBUBTUPSFͰGPMMPXͨ͠ 5PQJDͱ"VUIPSͷJEΛอଘ

Slide 37

Slide 37 text

App core-ui core-navigation feature-author sync feature-interests feature-foryou feature-topic core-model core-common core-data core- datastore core- database core- network ˎUFTU༻ͷNPEVMF͸ আ͍͍ͯ·͢ 3FUPSpUΛ࢖ͬͯωοτϫʔΫ͔Β σʔλΛऔಘ

Slide 38

Slide 38 text

App core-ui core-navigation feature-author sync feature-interests feature-foryou feature-topic core-model core-common core-data core- datastore core- database core- network ˎUFTU༻ͷNPEVMF͸ আ͍͍ͯ·͢ SPPNʹ5PQJD΍"VUIPSͳͲͷ σʔλΛอଘ

Slide 39

Slide 39 text

App core-ui core-navigation feature-author sync feature-interests feature-foryou feature-topic core-model core-common core-data core- datastore core- database core- network ˎUFTU༻ͷNPEVMF͸ আ͍͍ͯ·͢ 5PQJD "VUIPS /FXT ͷ3FQPTJUPSZ࣮૷

Slide 40

Slide 40 text

App core-ui core-navigation feature-author sync feature-interests feature-foryou feature-topic core-model core-common core-data core- datastore core- database core- network ˎUFTU༻ͷNPEVMF͸ আ͍͍ͯ·͢ 8PSL.BOBHFSΛ࢖ͬͯΞϓϦ ىಈ࣌ʹσʔλΛಉظ

Slide 41

Slide 41 text

App core-ui core-navigation feature-author sync feature-interests feature-foryou feature-topic core-model core-common core-data core- datastore core- database core- network ˎUFTU༻ͷNPEVMF͸ আ͍͍ͯ·͢ /BWJHBUJPOͷભҠઌΛදݱ͢Δ JOUFSGBDF

Slide 42

Slide 42 text

App core-ui core-navigation feature-author sync feature-interests feature-foryou feature-topic core-model core-common core-data core- datastore core- database core- network ˎUFTU༻ͷNPEVMF͸ আ͍͍ͯ·͢

Slide 43

Slide 43 text

GFBUVSF99NPEVMF w ػೳ୯Ґͷ6*NPEVMF w 99/BWJHBUJPOLU/BW(SBQI#VJMEFSʹ DPNQPTBCMFΛ௥Ճ͢ΔͨΊͷ֦ுؔ਺ w 994DSFFOLUը໘ͷ$PNQPTBCMF w 997JFX.PEFMLU994DSFFOͰར༻͞Ε Δ7JFX.PEFM

Slide 44

Slide 44 text

5PQJD/BWJHBUJPOLU object TopicDestination : NiaNavigationDestination { override val route = "topic_route" override val destination = "topic_destination" const val topicIdArg = "topicId" } fun NavGraphBuilder.topicGraph( onBackClick: () -> Unit ) { composable( route = "${TopicDestination.route}/{${TopicDestination.topicIdArg}}", arguments = listOf( navArgument(TopicDestination.topicIdArg) { type = NavType.StringType } ) ) { TopicRoute(onBackClick = onBackClick) } } /BW(SBQIʹ͜ͷGFBUVSFͷը໘Λ ભҠઌͱͯ͠௥Ճ͢ΔͨΊͷ֦ுؔ਺

Slide 45

Slide 45 text

@Composable fun NiaNavHost( … ) { NavHost( navController = navController, startDestination = startDestination, modifier = modifier, ) { … interestsGraph( …, nestedGraphs = { topicGraph(onBackClick = { navController.popBackStack() }) authorGraph(onBackClick = { navController.popBackStack() }) } ) } }

Slide 46

Slide 46 text

$PNQPTFͷQSBDUJDF

Slide 47

Slide 47 text

.BUFSJBMXJUI+FUQBDL$PNQPTF w EZOBNJDDPMPSTDIFNFʢ"OESPJEҎ߱ʣରԠ @Composable fun NiaTheme( darkTheme: Boolean = isSystemInDarkTheme(), dynamicColor: Boolean = false, androidTheme: Boolean = false, content: @Composable() () -> Unit ) { val colorScheme = when { dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { val context = LocalContext.current if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(co } androidTheme && darkTheme -> DarkAndroidColorScheme androidTheme -> LightAndroidColorScheme darkTheme -> DarkDefaultColorScheme else -> LightDefaultColorScheme }

Slide 48

Slide 48 text

*DPOΛ·ͱΊͨPCKFDU object NiaIcons { val AccountCircle = Icons.Outlined.AccountCircle val Add = Icons.Rounded.Add val ArrowBack = Icons.Rounded.ArrowBack val ArrowDropDown = Icons.Rounded.ArrowDropDown val ArrowDropUp = Icons.Rounded.ArrowDropUp val Bookmark = R.drawable.ic_bookmark … } Icon( painter = painterResource(id = NiaIcons.Bookmark), contentDescription = null ) Icon(imageVector = NiaIcons.Add, contentDescription = null)

Slide 49

Slide 49 text

8JOEPX*OTFUTͷѻ͍ w 8JOEPX*OTFUTTBGF%SBXJOHΛओʹ࢖༻ NiaNavRail( …, modifier = Modifier.safeDrawingPadding() ) NavigationBar( modifier = Modifier.windowInsetsPadding( WindowInsets.safeDrawing.only( WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom ) ), … ) {

Slide 50

Slide 50 text

8JOEPX4J[FDMBTT w ը໘෯Ͱ#PUUPN#BSͱ/BW3BJMΛग़͠෼͚ Scaffold( … bottomBar = { if (windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact) { NiaBottomBar( … ) } } ) { padding -> Row(…) { if (windowSizeClass.widthSizeClass != WindowWidthSizeClass.Compact) { NiaNavRail( … ) } … } BOESPJEYDPNQPTFNBUFSJBMNBUFSJBMXJOEPXTJ[FDMBTTBMQIB

Slide 51

Slide 51 text

8JOEPX4J[FDMBTT w ը໘෯Ͱ#PUUPN#BSͱ/BW3BJMΛग़͠෼͚ w هࣄҰཡͷ$PMVNO਺ val numberOfColumns = when (windowSizeClass.widthSizeClass) { WindowWidthSizeClass.Compact, WindowWidthSizeClass.Medium -> 1 else -> floor(maxWidth / 300.dp).toInt().coerceAtLeast(1) } BOESPJEYDPNQPTFNBUFSJBMNBUFSJBMXJOEPXTJ[FDMBTTBMQIB

Slide 52

Slide 52 text

/BWJHBUJPO4BWFE4UBUF)BOEMF composable( route = "${TopicDestination.route}/{${TopicDestination.topicIdArg}}", arguments = listOf( navArgument(TopicDestination.topicIdArg) { type = NavType.StringType } ) ) { TopicRoute(onBackClick = onBackClick) } @Composable fun TopicRoute( onBackClick: () -> Unit, modifier: Modifier = Modifier, viewModel: TopicViewModel = hiltViewModel(), ) { … 5PQJD%FTUJOBUJPOUPQJD*E"SH ͱ͍͏໊લͰ 4USJOH ͷBSHVNFOU /BW#BDL4UBDL&OUSZͷBSHVNFOUT͕ "CTUSBDU4BWFE4UBUF7JFX.PEFM'BDUPSZ ͷEFGBVMU"SHTʹ౉͞ΕΔ

Slide 53

Slide 53 text

/BWJHBUJPO4BWFE4UBUF)BOEMF @HiltViewModel class TopicViewModel @Inject constructor( savedStateHandle: SavedStateHandle, … ) : ViewModel() { private val topicId: String = checkNotNull(savedStateHandle[TopicDestination.topicIdArg]) … } 5PQJD%FTUJOBUJPOUPQJD*E"SH ͱ͍͏໊લͰ 4USJOH ͷBSHVNFOU ͕4BWFE4UBUF)BOEMFʹೖ͍ͬͯΔ

Slide 54

Slide 54 text

CZTBWFE4UBUF)BOEMFTBWFBCMF\^ TextField( value = viewModel.text, onValueChange = { viewModel.text = it } ) @HiltViewModel class SampleViewModel @Inject constructor( savedStateHandle: SavedStateHandle ) : ViewModel() { var text by mutableStateOf("") } 1SPDFTTLJMM࣌ʹ5FYU'JFMEͷ ೖྗจࣈΛࣦͬͯ͠·͏ @HiltViewModel class SampleViewModel @Inject constructor( savedStateHandle: SavedStateHandle ) : ViewModel() { var text by savedStateHandle.saveable { mutableStateOf("") } } 1SPDFTTLJMM࣌΋ೖྗจࣈ ͕อ࣋͞ΕΔ 😫 👍

Slide 55

Slide 55 text

·ͱΊ w /PXJO"OESPJEΞϓϦʹ͸࠷৽ͷ"OESPJE։ൃͷ஌ݟ͕੝Γࠐ·Ε͍ͯΔ w ίʔυΛಡΜͰΈΑ͏ w IUUQTHJUIVCDPNBOESPJEOPXJOBOESPJE