diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index a78ae80b..e13ea9f4 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,7 +1,9 @@
+@file:Suppress("INLINE_FROM_HIGHER_PLATFORM")
+
plugins {
alias(libs.plugins.booket.android.application)
- alias(libs.plugins.booket.android.hilt)
alias(libs.plugins.booket.android.application.compose)
+ alias(libs.plugins.booket.android.hilt)
}
android {
@@ -22,20 +24,25 @@ android {
}
}
+ksp {
+ arg("circuit.codegen.mode", "hilt")
+}
+
dependencies {
+ implementations(
+ projects.core.designsystem,
+
+ projects.feature.home,
+ projects.feature.library,
+ projects.feature.main,
+ projects.feature.search,
+
+ libs.androidx.activity.compose,
+ libs.androidx.startup,
+ libs.logger,
- implementation(libs.androidx.core.ktx)
- implementation(libs.androidx.lifecycle.runtime.ktx)
- implementation(libs.androidx.activity.compose)
- implementation(platform(libs.androidx.compose.bom))
- implementation(libs.androidx.compose.ui)
- implementation(libs.androidx.compose.ui.graphics)
- implementation(libs.androidx.compose.ui.tooling.preview)
- implementation(libs.androidx.compose.material3)
- testImplementation(libs.junit)
- androidTestImplementation(libs.androidx.junit)
- androidTestImplementation(platform(libs.androidx.compose.bom))
- androidTestImplementation(libs.androidx.compose.ui.test.junit4)
- debugImplementation(libs.androidx.compose.ui.tooling)
- debugImplementation(libs.androidx.compose.ui.test.manifest)
+ libs.bundles.circuit,
+ )
+ api(libs.circuit.codegen.annotation)
+ ksp(libs.circuit.codegen.ksp)
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7b995aa0..59ace20d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools">
-
-
-
-
-
-
diff --git a/app/src/main/kotlin/com/ninecraft/booket/BooketApplication.kt b/app/src/main/kotlin/com/ninecraft/booket/BooketApplication.kt
new file mode 100644
index 00000000..d12164ee
--- /dev/null
+++ b/app/src/main/kotlin/com/ninecraft/booket/BooketApplication.kt
@@ -0,0 +1,7 @@
+package com.ninecraft.booket
+
+import android.app.Application
+import dagger.hilt.android.HiltAndroidApp
+
+@HiltAndroidApp
+class BooketApplication : Application()
diff --git a/app/src/main/kotlin/com/ninecraft/booket/MainActivity.kt b/app/src/main/kotlin/com/ninecraft/booket/MainActivity.kt
deleted file mode 100644
index 06318c76..00000000
--- a/app/src/main/kotlin/com/ninecraft/booket/MainActivity.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-package com.ninecraft.booket
-
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.activity.enableEdgeToEdge
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.tooling.preview.Preview
-import com.ninecraft.booket.ui.theme.BooketAndroidTheme
-
-class MainActivity : ComponentActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- enableEdgeToEdge()
- setContent {
- BooketAndroidTheme {
- Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
- Greeting(
- name = "Android",
- modifier = Modifier.padding(innerPadding),
- )
- }
- }
- }
- }
-}
-
-@Composable
-fun Greeting(name: String, modifier: Modifier = Modifier) {
- Text(
- text = "Hello $name!",
- modifier = modifier,
- )
-}
-
-@Preview(showBackground = true)
-@Composable
-fun GreetingPreview() {
- BooketAndroidTheme {
- Greeting("Android")
- }
-}
diff --git a/app/src/main/kotlin/com/ninecraft/booket/di/CircuitModule.kt b/app/src/main/kotlin/com/ninecraft/booket/di/CircuitModule.kt
new file mode 100644
index 00000000..6514c77e
--- /dev/null
+++ b/app/src/main/kotlin/com/ninecraft/booket/di/CircuitModule.kt
@@ -0,0 +1,33 @@
+package com.ninecraft.booket.di
+
+import com.slack.circuit.foundation.Circuit
+import com.slack.circuit.runtime.presenter.Presenter
+import com.slack.circuit.runtime.ui.Ui
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ActivityRetainedComponent
+import dagger.hilt.android.scopes.ActivityRetainedScoped
+import dagger.multibindings.Multibinds
+
+@Module
+@InstallIn(ActivityRetainedComponent::class)
+abstract class CircuitModule {
+
+ @Multibinds
+ abstract fun presenterFactories(): Set
+
+ @Multibinds
+ abstract fun uiFactories(): Set
+
+ companion object {
+ @[Provides ActivityRetainedScoped]
+ fun provideCircuit(
+ presenterFactories: @JvmSuppressWildcards Set,
+ uiFactories: @JvmSuppressWildcards Set,
+ ): Circuit = Circuit.Builder()
+ .addPresenterFactories(presenterFactories)
+ .addUiFactories(uiFactories)
+ .build()
+ }
+}
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
deleted file mode 100644
index 55fe981b..00000000
--- a/app/src/main/res/values/strings.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
- Booket-Android
-
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
deleted file mode 100644
index f9c05e74..00000000
--- a/app/src/main/res/values/themes.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts
index cd8ce549..d377efa3 100644
--- a/build-logic/build.gradle.kts
+++ b/build-logic/build.gradle.kts
@@ -1,15 +1,14 @@
-@Suppress("DSL_SCOPE_VIOLATION")
+@Suppress("DSL_SCOPE_VIOLATION", "INLINE_FROM_HIGHER_PLATFORM")
plugins {
`kotlin-dsl`
alias(libs.plugins.gradle.dependency.handler.extensions)
}
-group = "com.ninecraft.booket.buildlogic"
-
dependencies {
compileOnly(libs.android.gradle.plugin)
compileOnly(libs.kotlin.gradle.plugin)
+ compileOnly(libs.compose.compiler.gradle.plugin)
implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location))
}
@@ -32,16 +31,6 @@ gradlePlugin {
}
}
-java {
- toolchain {
- languageVersion.set(JavaLanguageVersion.of(17))
- }
-}
-
-kotlin {
- jvmToolchain(17)
-}
-
// Pair
fun NamedDomainObjectContainer.pluginRegister(data: Pair) {
val (pluginName, className) = data
diff --git a/build-logic/src/main/java/AndroidFeatureConventionPlugin.kt b/build-logic/src/main/java/AndroidFeatureConventionPlugin.kt
deleted file mode 100644
index d9b88f47..00000000
--- a/build-logic/src/main/java/AndroidFeatureConventionPlugin.kt
+++ /dev/null
@@ -1,20 +0,0 @@
-import com.ninecraft.booket.convention.applyPlugins
-import org.gradle.api.Plugin
-import org.gradle.api.Project
-import org.gradle.kotlin.dsl.dependencies
-
-internal class AndroidFeatureConventionPlugin : Plugin {
- override fun apply(target: Project) {
- with(target) {
- applyPlugins(
- "booket-android-library",
- "booket-android-hilt",
- "booket-android-library-compose",
- )
-
- dependencies {
-
- }
- }
- }
-}
diff --git a/build-logic/src/main/java/com/ninecraft/booket/convention/Compose.kt b/build-logic/src/main/java/com/ninecraft/booket/convention/Compose.kt
deleted file mode 100644
index 66490d9b..00000000
--- a/build-logic/src/main/java/com/ninecraft/booket/convention/Compose.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package com.ninecraft.booket.convention
-
-import com.android.build.api.dsl.CommonExtension
-import org.gradle.api.Project
-import org.gradle.kotlin.dsl.dependencies
-
-internal fun Project.configureCompose(
- extension: CommonExtension<*, *, *, *, *, *>,
-) {
- applyPlugins(
- Plugins.KOTLIN_COMPOSE,
- )
-
- extension.apply {
- dependencies {
- implementation(platform(libs.androidx.compose.bom))
- implementation(libs.bundles.androidx.compose)
- debugImplementation(libs.androidx.compose.ui.tooling)
- }
- }
-}
diff --git a/build-logic/src/main/java/AndroidApplicationComposeConventionPlugin.kt b/build-logic/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt
similarity index 94%
rename from build-logic/src/main/java/AndroidApplicationComposeConventionPlugin.kt
rename to build-logic/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt
index 393d0ec5..310243b4 100644
--- a/build-logic/src/main/java/AndroidApplicationComposeConventionPlugin.kt
+++ b/build-logic/src/main/kotlin/AndroidApplicationComposeConventionPlugin.kt
@@ -11,6 +11,7 @@ internal class AndroidApplicationComposeConventionPlugin : Plugin {
with(target) {
applyPlugins(
Plugins.ANDROID_APPLICATION,
+ Plugins.KOTLIN_COMPOSE,
)
extensions.configure {
diff --git a/build-logic/src/main/java/AndroidApplicationConventionPlugin.kt b/build-logic/src/main/kotlin/AndroidApplicationConventionPlugin.kt
similarity index 100%
rename from build-logic/src/main/java/AndroidApplicationConventionPlugin.kt
rename to build-logic/src/main/kotlin/AndroidApplicationConventionPlugin.kt
diff --git a/build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt b/build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt
new file mode 100644
index 00000000..738aa539
--- /dev/null
+++ b/build-logic/src/main/kotlin/AndroidFeatureConventionPlugin.kt
@@ -0,0 +1,30 @@
+import com.ninecraft.booket.convention.applyPlugins
+import com.ninecraft.booket.convention.implementation
+import com.ninecraft.booket.convention.api
+import com.ninecraft.booket.convention.ksp
+import com.ninecraft.booket.convention.project
+import com.ninecraft.booket.convention.libs
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.dependencies
+
+internal class AndroidFeatureConventionPlugin : Plugin {
+ override fun apply(target: Project) {
+ with(target) {
+ applyPlugins(
+ "booket.android.library",
+ "booket.android.library.compose",
+ "booket.android.hilt",
+ )
+
+ dependencies {
+ implementation(project(path = ":core:designsystem"))
+
+ implementation(libs.bundles.circuit)
+
+ api(libs.circuit.codegen.annotation)
+ ksp(libs.circuit.codegen.ksp)
+ }
+ }
+ }
+}
diff --git a/build-logic/src/main/java/AndroidHiltConventionPlugin.kt b/build-logic/src/main/kotlin/AndroidHiltConventionPlugin.kt
similarity index 93%
rename from build-logic/src/main/java/AndroidHiltConventionPlugin.kt
rename to build-logic/src/main/kotlin/AndroidHiltConventionPlugin.kt
index 422d5dd7..0875e71a 100644
--- a/build-logic/src/main/java/AndroidHiltConventionPlugin.kt
+++ b/build-logic/src/main/kotlin/AndroidHiltConventionPlugin.kt
@@ -17,7 +17,7 @@ internal class AndroidHiltConventionPlugin : Plugin {
dependencies {
implementation(libs.hilt.android)
- ksp(libs.hilt.compiler)
+ ksp(libs.hilt.android.compiler)
}
}
}
diff --git a/build-logic/src/main/java/AndroidLibraryComposeConventionPlugin.kt b/build-logic/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt
similarity index 94%
rename from build-logic/src/main/java/AndroidLibraryComposeConventionPlugin.kt
rename to build-logic/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt
index 0bc44778..8096e831 100644
--- a/build-logic/src/main/java/AndroidLibraryComposeConventionPlugin.kt
+++ b/build-logic/src/main/kotlin/AndroidLibraryComposeConventionPlugin.kt
@@ -11,6 +11,7 @@ class AndroidLibraryComposeConventionPlugin : Plugin {
with(target) {
applyPlugins(
Plugins.ANDROID_LIBRARY,
+ Plugins.KOTLIN_COMPOSE,
)
extensions.configure {
diff --git a/build-logic/src/main/java/AndroidLibraryConventionPlugin.kt b/build-logic/src/main/kotlin/AndroidLibraryConventionPlugin.kt
similarity index 78%
rename from build-logic/src/main/java/AndroidLibraryConventionPlugin.kt
rename to build-logic/src/main/kotlin/AndroidLibraryConventionPlugin.kt
index 75fba3d0..39888c94 100644
--- a/build-logic/src/main/java/AndroidLibraryConventionPlugin.kt
+++ b/build-logic/src/main/kotlin/AndroidLibraryConventionPlugin.kt
@@ -1,10 +1,11 @@
import com.android.build.gradle.LibraryExtension
import com.ninecraft.booket.convention.Plugins
-import org.gradle.api.Plugin
-import org.gradle.api.Project
import com.ninecraft.booket.convention.applyPlugins
import com.ninecraft.booket.convention.configureAndroid
+import org.gradle.api.Plugin
+import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
+import com.ninecraft.booket.convention.ApplicationConstants
internal class AndroidLibraryConventionPlugin : Plugin {
override fun apply(target: Project) {
@@ -16,6 +17,10 @@ internal class AndroidLibraryConventionPlugin : Plugin {
extensions.configure {
configureAndroid(this)
+
+ defaultConfig.apply {
+ targetSdk = ApplicationConstants.TARGET_SDK
+ }
}
}
}
diff --git a/build-logic/src/main/java/JvmLibraryConventionPlugin.kt b/build-logic/src/main/kotlin/JvmLibraryConventionPlugin.kt
similarity index 100%
rename from build-logic/src/main/java/JvmLibraryConventionPlugin.kt
rename to build-logic/src/main/kotlin/JvmLibraryConventionPlugin.kt
diff --git a/build-logic/src/main/java/KotlinLibrarySerializationConventionPlugin.kt b/build-logic/src/main/kotlin/KotlinLibrarySerializationConventionPlugin.kt
similarity index 100%
rename from build-logic/src/main/java/KotlinLibrarySerializationConventionPlugin.kt
rename to build-logic/src/main/kotlin/KotlinLibrarySerializationConventionPlugin.kt
diff --git a/build-logic/src/main/java/com/ninecraft/booket/convention/Android.kt b/build-logic/src/main/kotlin/com/ninecraft/booket/convention/Android.kt
similarity index 100%
rename from build-logic/src/main/java/com/ninecraft/booket/convention/Android.kt
rename to build-logic/src/main/kotlin/com/ninecraft/booket/convention/Android.kt
diff --git a/build-logic/src/main/java/com/ninecraft/booket/convention/ApplicationConstants.kt b/build-logic/src/main/kotlin/com/ninecraft/booket/convention/ApplicationConstants.kt
similarity index 100%
rename from build-logic/src/main/java/com/ninecraft/booket/convention/ApplicationConstants.kt
rename to build-logic/src/main/kotlin/com/ninecraft/booket/convention/ApplicationConstants.kt
diff --git a/build-logic/src/main/kotlin/com/ninecraft/booket/convention/Compose.kt b/build-logic/src/main/kotlin/com/ninecraft/booket/convention/Compose.kt
new file mode 100644
index 00000000..2d5831bd
--- /dev/null
+++ b/build-logic/src/main/kotlin/com/ninecraft/booket/convention/Compose.kt
@@ -0,0 +1,62 @@
+package com.ninecraft.booket.convention
+
+import com.android.build.api.dsl.CommonExtension
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.configure
+import org.gradle.kotlin.dsl.dependencies
+import org.gradle.kotlin.dsl.withType
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+import org.jetbrains.kotlin.compose.compiler.gradle.ComposeCompilerGradlePluginExtension
+
+internal fun Project.configureCompose(
+ extension: CommonExtension<*, *, *, *, *, *>,
+) {
+ extension.apply {
+ dependencies {
+ implementation(platform(libs.androidx.compose.bom))
+ implementation(libs.bundles.androidx.compose)
+ debugImplementation(libs.androidx.compose.ui.tooling)
+ }
+
+ configure {
+ includeSourceInformation.set(true)
+
+ metricsDestination.file("build/composeMetrics")
+ reportsDestination.file("build/composeReports")
+
+ stabilityConfigurationFiles.addAll(
+ project.layout.projectDirectory.file("stability.config.conf"),
+ )
+ }
+
+ tasks.withType().configureEach {
+ compilerOptions {
+ freeCompilerArgs.addAll(
+ buildComposeMetricsParameters(),
+ )
+ }
+ }
+ }
+}
+
+private fun Project.buildComposeMetricsParameters(): List {
+ val metricParameters = mutableListOf()
+ val enableMetricsProvider = project.providers.gradleProperty("enableComposeCompilerMetrics")
+ val relativePath = projectDir.relativeTo(rootDir)
+ val buildDir = layout.buildDirectory.get().asFile
+ val enableMetrics = (enableMetricsProvider.orNull == "true")
+ if (enableMetrics) {
+ val metricsFolder = buildDir.resolve("compose-metrics").resolve(relativePath)
+ metricParameters.add("-P")
+ metricParameters.add("plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + metricsFolder.absolutePath)
+ }
+
+ val enableReportsProvider = project.providers.gradleProperty("enableComposeCompilerReports")
+ val enableReports = (enableReportsProvider.orNull == "true")
+ if (enableReports) {
+ val reportsFolder = buildDir.resolve("compose-reports").resolve(relativePath)
+ metricParameters.add("-P")
+ metricParameters.add("plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + reportsFolder.absolutePath)
+ }
+ return metricParameters.toList()
+}
diff --git a/build-logic/src/main/java/com/ninecraft/booket/convention/Dependencies.kt b/build-logic/src/main/kotlin/com/ninecraft/booket/convention/Dependencies.kt
similarity index 100%
rename from build-logic/src/main/java/com/ninecraft/booket/convention/Dependencies.kt
rename to build-logic/src/main/kotlin/com/ninecraft/booket/convention/Dependencies.kt
diff --git a/build-logic/src/main/java/com/ninecraft/booket/convention/Extensions.kt b/build-logic/src/main/kotlin/com/ninecraft/booket/convention/Extensions.kt
similarity index 100%
rename from build-logic/src/main/java/com/ninecraft/booket/convention/Extensions.kt
rename to build-logic/src/main/kotlin/com/ninecraft/booket/convention/Extensions.kt
diff --git a/build-logic/src/main/java/com/ninecraft/booket/convention/Plugins.kt b/build-logic/src/main/kotlin/com/ninecraft/booket/convention/Plugins.kt
similarity index 100%
rename from build-logic/src/main/java/com/ninecraft/booket/convention/Plugins.kt
rename to build-logic/src/main/kotlin/com/ninecraft/booket/convention/Plugins.kt
diff --git a/build.gradle.kts b/build.gradle.kts
index 6158bf27..545ef068 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -5,11 +5,12 @@ plugins {
alias(libs.plugins.gradle.dependency.handler.extensions)
alias(libs.plugins.kotlin.detekt)
alias(libs.plugins.kotlin.ktlint)
- alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.kotlin.parcelize) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
- alias(libs.plugins.android.library) apply false
alias(libs.plugins.kotlin.serialization) apply false
+ alias(libs.plugins.android.application) apply false
+ alias(libs.plugins.android.library) apply false
alias(libs.plugins.hilt) apply false
alias(libs.plugins.ksp) apply false
}
diff --git a/core/designsystem/.gitignore b/core/designsystem/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/core/designsystem/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/core/designsystem/build.gradle.kts b/core/designsystem/build.gradle.kts
new file mode 100644
index 00000000..100729c4
--- /dev/null
+++ b/core/designsystem/build.gradle.kts
@@ -0,0 +1,16 @@
+@file:Suppress("INLINE_FROM_HIGHER_PLATFORM")
+
+plugins {
+ alias(libs.plugins.booket.android.library)
+ alias(libs.plugins.booket.android.library.compose)
+}
+
+android {
+ namespace = "com.ninecraft.booket.core.designsystem"
+}
+
+dependencies {
+ implementations(
+ libs.logger,
+ )
+}
diff --git a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/ComponentPreview.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/ComponentPreview.kt
new file mode 100644
index 00000000..06580453
--- /dev/null
+++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/ComponentPreview.kt
@@ -0,0 +1,17 @@
+package com.ninecraft.booket.core.designsystem
+
+import android.content.res.Configuration.UI_MODE_NIGHT_NO
+import android.content.res.Configuration.UI_MODE_NIGHT_YES
+import androidx.compose.ui.tooling.preview.Preview
+
+@Preview(
+ name = "Light",
+ showBackground = true,
+ uiMode = UI_MODE_NIGHT_NO,
+)
+@Preview(
+ name = "Dark",
+ showBackground = true,
+ uiMode = UI_MODE_NIGHT_YES,
+)
+annotation class ComponentPreview
diff --git a/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/DevicePreview.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/DevicePreview.kt
new file mode 100644
index 00000000..8dbd713d
--- /dev/null
+++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/DevicePreview.kt
@@ -0,0 +1,19 @@
+package com.ninecraft.booket.core.designsystem
+
+import android.content.res.Configuration.UI_MODE_NIGHT_NO
+import android.content.res.Configuration.UI_MODE_NIGHT_YES
+import androidx.compose.ui.tooling.preview.Preview
+
+@Preview(
+ name = "Light",
+ showBackground = true,
+ uiMode = UI_MODE_NIGHT_NO,
+ device = "spec:width=360dp,height=800dp,dpi=411",
+)
+@Preview(
+ name = "Dark",
+ showBackground = true,
+ uiMode = UI_MODE_NIGHT_YES,
+ device = "spec:width=360dp,height=800dp,dpi=411",
+)
+annotation class DevicePreview
diff --git a/app/src/main/kotlin/com/ninecraft/booket/ui/theme/Color.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Color.kt
similarity index 82%
rename from app/src/main/kotlin/com/ninecraft/booket/ui/theme/Color.kt
rename to core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Color.kt
index 8edd69d2..62b549bf 100644
--- a/app/src/main/kotlin/com/ninecraft/booket/ui/theme/Color.kt
+++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Color.kt
@@ -1,4 +1,4 @@
-package com.ninecraft.booket.ui.theme
+package com.ninecraft.booket.core.designsystem.theme
import androidx.compose.ui.graphics.Color
diff --git a/app/src/main/kotlin/com/ninecraft/booket/ui/theme/Theme.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Theme.kt
similarity index 95%
rename from app/src/main/kotlin/com/ninecraft/booket/ui/theme/Theme.kt
rename to core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Theme.kt
index 52446a91..948b8f61 100644
--- a/app/src/main/kotlin/com/ninecraft/booket/ui/theme/Theme.kt
+++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Theme.kt
@@ -1,4 +1,4 @@
-package com.ninecraft.booket.ui.theme
+package com.ninecraft.booket.core.designsystem.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
@@ -34,7 +34,7 @@ private val LightColorScheme = lightColorScheme(
)
@Composable
-fun BooketAndroidTheme(
+fun BooketTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
diff --git a/app/src/main/kotlin/com/ninecraft/booket/ui/theme/Type.kt b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Type.kt
similarity index 94%
rename from app/src/main/kotlin/com/ninecraft/booket/ui/theme/Type.kt
rename to core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Type.kt
index b19900fa..82e41366 100644
--- a/app/src/main/kotlin/com/ninecraft/booket/ui/theme/Type.kt
+++ b/core/designsystem/src/main/kotlin/com/ninecraft/booket/core/designsystem/theme/Type.kt
@@ -1,4 +1,4 @@
-package com.ninecraft.booket.ui.theme
+package com.ninecraft.booket.core.designsystem.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/core/designsystem/src/main/res/drawable/ic_launcher_background.xml
similarity index 100%
rename from app/src/main/res/drawable/ic_launcher_background.xml
rename to core/designsystem/src/main/res/drawable/ic_launcher_background.xml
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/core/designsystem/src/main/res/drawable/ic_launcher_foreground.xml
similarity index 100%
rename from app/src/main/res/drawable/ic_launcher_foreground.xml
rename to core/designsystem/src/main/res/drawable/ic_launcher_foreground.xml
diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/core/designsystem/src/main/res/mipmap-anydpi/ic_launcher.xml
similarity index 100%
rename from app/src/main/res/mipmap-anydpi/ic_launcher.xml
rename to core/designsystem/src/main/res/mipmap-anydpi/ic_launcher.xml
diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/core/designsystem/src/main/res/mipmap-anydpi/ic_launcher_round.xml
similarity index 100%
rename from app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
rename to core/designsystem/src/main/res/mipmap-anydpi/ic_launcher_round.xml
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/core/designsystem/src/main/res/mipmap-hdpi/ic_launcher.webp
similarity index 100%
rename from app/src/main/res/mipmap-hdpi/ic_launcher.webp
rename to core/designsystem/src/main/res/mipmap-hdpi/ic_launcher.webp
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/core/designsystem/src/main/res/mipmap-hdpi/ic_launcher_round.webp
similarity index 100%
rename from app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
rename to core/designsystem/src/main/res/mipmap-hdpi/ic_launcher_round.webp
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/core/designsystem/src/main/res/mipmap-mdpi/ic_launcher.webp
similarity index 100%
rename from app/src/main/res/mipmap-mdpi/ic_launcher.webp
rename to core/designsystem/src/main/res/mipmap-mdpi/ic_launcher.webp
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/core/designsystem/src/main/res/mipmap-mdpi/ic_launcher_round.webp
similarity index 100%
rename from app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
rename to core/designsystem/src/main/res/mipmap-mdpi/ic_launcher_round.webp
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/core/designsystem/src/main/res/mipmap-xhdpi/ic_launcher.webp
similarity index 100%
rename from app/src/main/res/mipmap-xhdpi/ic_launcher.webp
rename to core/designsystem/src/main/res/mipmap-xhdpi/ic_launcher.webp
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/core/designsystem/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
similarity index 100%
rename from app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
rename to core/designsystem/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/core/designsystem/src/main/res/mipmap-xxhdpi/ic_launcher.webp
similarity index 100%
rename from app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
rename to core/designsystem/src/main/res/mipmap-xxhdpi/ic_launcher.webp
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/core/designsystem/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
similarity index 100%
rename from app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
rename to core/designsystem/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/core/designsystem/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
similarity index 100%
rename from app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
rename to core/designsystem/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/core/designsystem/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
similarity index 100%
rename from app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
rename to core/designsystem/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
diff --git a/app/src/main/res/values/colors.xml b/core/designsystem/src/main/res/values/colors.xml
similarity index 100%
rename from app/src/main/res/values/colors.xml
rename to core/designsystem/src/main/res/values/colors.xml
diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/main/res/values/strings.xml
new file mode 100644
index 00000000..ee421881
--- /dev/null
+++ b/core/designsystem/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Booket
+
diff --git a/core/designsystem/src/main/res/values/themes.xml b/core/designsystem/src/main/res/values/themes.xml
new file mode 100644
index 00000000..67a14ca0
--- /dev/null
+++ b/core/designsystem/src/main/res/values/themes.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/xml/backup_rules.xml b/core/designsystem/src/main/res/xml/backup_rules.xml
similarity index 100%
rename from app/src/main/res/xml/backup_rules.xml
rename to core/designsystem/src/main/res/xml/backup_rules.xml
diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/core/designsystem/src/main/res/xml/data_extraction_rules.xml
similarity index 100%
rename from app/src/main/res/xml/data_extraction_rules.xml
rename to core/designsystem/src/main/res/xml/data_extraction_rules.xml
diff --git a/feature/home/.gitignore b/feature/home/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/feature/home/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/feature/home/build.gradle.kts b/feature/home/build.gradle.kts
new file mode 100644
index 00000000..f511ebd6
--- /dev/null
+++ b/feature/home/build.gradle.kts
@@ -0,0 +1,25 @@
+@file:Suppress("INLINE_FROM_HIGHER_PLATFORM")
+
+plugins {
+ alias(libs.plugins.booket.android.feature)
+ alias(libs.plugins.kotlin.serialization)
+ alias(libs.plugins.kotlin.parcelize)
+}
+
+android {
+ namespace = "com.ninecraft.booket.feature.home"
+
+ buildFeatures {
+ buildConfig = true
+ }
+}
+
+ksp {
+ arg("circuit.codegen.mode", "hilt")
+}
+
+dependencies {
+ implementations(
+ libs.logger,
+ )
+}
diff --git a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt
new file mode 100644
index 00000000..f36fd434
--- /dev/null
+++ b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomePresenter.kt
@@ -0,0 +1,30 @@
+package com.ninecraft.booket.feature.home
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import com.slack.circuit.codegen.annotations.CircuitInject
+import com.slack.circuit.runtime.Navigator
+import com.slack.circuit.runtime.presenter.Presenter
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import dagger.hilt.android.components.ActivityRetainedComponent
+
+@Suppress("unused")
+class HomePresenter @AssistedInject constructor(
+ @Assisted private val navigator: Navigator,
+) : Presenter {
+
+ @Composable
+ override fun present(): HomeScreen.State {
+ val scope = rememberCoroutineScope()
+
+ return HomeScreen.State {}
+ }
+
+ @CircuitInject(HomeScreen::class, ActivityRetainedComponent::class)
+ @AssistedFactory
+ fun interface Factory {
+ fun create(navigator: Navigator): HomePresenter
+ }
+}
diff --git a/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeScreen.kt b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeScreen.kt
new file mode 100644
index 00000000..0a3477b1
--- /dev/null
+++ b/feature/home/src/main/kotlin/com/ninecraft/booket/feature/home/HomeScreen.kt
@@ -0,0 +1,65 @@
+package com.ninecraft.booket.feature.home
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import com.ninecraft.booket.core.designsystem.DevicePreview
+import com.ninecraft.booket.core.designsystem.theme.BooketTheme
+import com.slack.circuit.codegen.annotations.CircuitInject
+import com.slack.circuit.runtime.CircuitUiEvent
+import com.slack.circuit.runtime.CircuitUiState
+import com.slack.circuit.runtime.screen.Screen
+import dagger.hilt.android.components.ActivityRetainedComponent
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data object HomeScreen : Screen {
+ data class State(
+ val eventSink: (Event) -> Unit,
+ ) : CircuitUiState
+
+ sealed interface Event : CircuitUiEvent
+}
+
+@CircuitInject(HomeScreen::class, ActivityRetainedComponent::class)
+@Composable
+internal fun Home(
+ state: HomeScreen.State,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ HomeContent(
+ state = state,
+ modifier = modifier,
+ )
+ }
+}
+
+@Suppress("unused")
+@Composable
+internal fun HomeContent(
+ state: HomeScreen.State,
+ modifier: Modifier = Modifier,
+) {
+ Text(text = "홈")
+}
+
+@DevicePreview
+@Composable
+private fun HomePreview() {
+ BooketTheme {
+ Home(
+ state = HomeScreen.State(
+ eventSink = {},
+ ),
+ )
+ }
+}
diff --git a/feature/library/.gitignore b/feature/library/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/feature/library/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/feature/library/build.gradle.kts b/feature/library/build.gradle.kts
new file mode 100644
index 00000000..338139f2
--- /dev/null
+++ b/feature/library/build.gradle.kts
@@ -0,0 +1,25 @@
+@file:Suppress("INLINE_FROM_HIGHER_PLATFORM")
+
+plugins {
+ alias(libs.plugins.booket.android.feature)
+ alias(libs.plugins.kotlin.serialization)
+ alias(libs.plugins.kotlin.parcelize)
+}
+
+android {
+ namespace = "com.ninecraft.booket.feature.library"
+
+ buildFeatures {
+ buildConfig = true
+ }
+}
+
+ksp {
+ arg("circuit.codegen.mode", "hilt")
+}
+
+dependencies {
+ implementations(
+ libs.logger,
+ )
+}
diff --git a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt
new file mode 100644
index 00000000..836e4f6d
--- /dev/null
+++ b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryPresenter.kt
@@ -0,0 +1,30 @@
+package com.ninecraft.booket.feature.library
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import com.slack.circuit.codegen.annotations.CircuitInject
+import com.slack.circuit.runtime.Navigator
+import com.slack.circuit.runtime.presenter.Presenter
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import dagger.hilt.android.components.ActivityRetainedComponent
+
+@Suppress("unused")
+class LibraryPresenter @AssistedInject constructor(
+ @Assisted private val navigator: Navigator,
+) : Presenter {
+
+ @Composable
+ override fun present(): LibraryScreen.State {
+ val scope = rememberCoroutineScope()
+
+ return LibraryScreen.State {}
+ }
+
+ @CircuitInject(LibraryScreen::class, ActivityRetainedComponent::class)
+ @AssistedFactory
+ fun interface Factory {
+ fun create(navigator: Navigator): LibraryPresenter
+ }
+}
diff --git a/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryScreen.kt b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryScreen.kt
new file mode 100644
index 00000000..d7ec79a0
--- /dev/null
+++ b/feature/library/src/main/kotlin/com/ninecraft/booket/feature/library/LibraryScreen.kt
@@ -0,0 +1,65 @@
+package com.ninecraft.booket.feature.library
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import com.ninecraft.booket.core.designsystem.DevicePreview
+import com.ninecraft.booket.core.designsystem.theme.BooketTheme
+import com.slack.circuit.codegen.annotations.CircuitInject
+import com.slack.circuit.runtime.CircuitUiEvent
+import com.slack.circuit.runtime.CircuitUiState
+import com.slack.circuit.runtime.screen.Screen
+import dagger.hilt.android.components.ActivityRetainedComponent
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data object LibraryScreen : Screen {
+ data class State(
+ val eventSink: (Event) -> Unit,
+ ) : CircuitUiState
+
+ sealed interface Event : CircuitUiEvent
+}
+
+@CircuitInject(LibraryScreen::class, ActivityRetainedComponent::class)
+@Composable
+internal fun Library(
+ state: LibraryScreen.State,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ LibraryContent(
+ state = state,
+ modifier = modifier,
+ )
+ }
+}
+
+@Suppress("unused")
+@Composable
+internal fun LibraryContent(
+ state: LibraryScreen.State,
+ modifier: Modifier = Modifier,
+) {
+ Text(text = "내 서재")
+}
+
+@DevicePreview
+@Composable
+private fun LibraryPreview() {
+ BooketTheme {
+ Library(
+ state = LibraryScreen.State(
+ eventSink = {},
+ ),
+ )
+ }
+}
diff --git a/feature/main/.gitignore b/feature/main/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/feature/main/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/feature/main/build.gradle.kts b/feature/main/build.gradle.kts
new file mode 100644
index 00000000..f29eaf7d
--- /dev/null
+++ b/feature/main/build.gradle.kts
@@ -0,0 +1,27 @@
+@file:Suppress("INLINE_FROM_HIGHER_PLATFORM")
+
+plugins {
+ alias(libs.plugins.booket.android.feature)
+ alias(libs.plugins.kotlin.serialization)
+}
+
+android {
+ namespace = "com.ninecraft.booket.feature.main"
+}
+
+ksp {
+ arg("circuit.codegen.mode", "hilt")
+}
+
+dependencies {
+ implementations(
+ projects.feature.home,
+ projects.feature.library,
+ projects.feature.search,
+
+ libs.kotlinx.collections.immutable,
+
+ libs.androidx.activity.compose,
+ libs.androidx.splash,
+ )
+}
diff --git a/feature/main/src/main/AndroidManifest.xml b/feature/main/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..18fa46a9
--- /dev/null
+++ b/feature/main/src/main/AndroidManifest.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/MainActivity.kt b/feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/MainActivity.kt
new file mode 100644
index 00000000..492a221e
--- /dev/null
+++ b/feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/MainActivity.kt
@@ -0,0 +1,59 @@
+package com.ninecraft.booket.feature.main
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Scaffold
+import androidx.compose.ui.Modifier
+import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
+import com.ninecraft.booket.feature.home.HomeScreen
+import com.ninecraft.booket.feature.main.component.MainBottomBar
+import com.slack.circuit.backstack.rememberSaveableBackStack
+import com.slack.circuit.foundation.Circuit
+import com.slack.circuit.foundation.CircuitCompositionLocals
+import com.slack.circuit.foundation.NavigableCircuitContent
+import com.slack.circuit.foundation.rememberCircuitNavigator
+import com.slack.circuit.overlay.ContentWithOverlays
+import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class MainActivity : ComponentActivity() {
+ @Inject
+ lateinit var circuit: Circuit
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ enableEdgeToEdge()
+ installSplashScreen()
+ super.onCreate(savedInstanceState)
+ setContent {
+ val backStack = rememberSaveableBackStack(root = HomeScreen)
+ val navigator = rememberCircuitNavigator(backStack)
+
+ CircuitCompositionLocals(circuit) {
+ Scaffold(
+ modifier = Modifier.fillMaxSize(),
+ bottomBar = {
+ MainBottomBar(
+ navigator = navigator,
+ backStack = backStack,
+ )
+ },
+ ) { innerPadding ->
+ ContentWithOverlays {
+ NavigableCircuitContent(
+ navigator = navigator,
+ backStack = backStack,
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(innerPadding),
+ )
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/component/MainBottomBar.kt b/feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/component/MainBottomBar.kt
new file mode 100644
index 00000000..b960a784
--- /dev/null
+++ b/feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/component/MainBottomBar.kt
@@ -0,0 +1,182 @@
+package com.ninecraft.booket.feature.main.component
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideIn
+import androidx.compose.animation.slideOut
+import androidx.compose.foundation.background
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.selection.selectable
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import com.ninecraft.booket.core.designsystem.ComponentPreview
+import com.ninecraft.booket.feature.home.HomeScreen
+import com.ninecraft.booket.feature.library.LibraryScreen
+import com.ninecraft.booket.feature.search.SearchScreen
+import com.ninecraft.booket.core.designsystem.theme.BooketTheme
+import com.slack.circuit.backstack.SaveableBackStack
+import com.slack.circuit.runtime.Navigator
+import com.slack.circuit.runtime.popUntil
+import com.slack.circuit.runtime.screen.Screen
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.toImmutableList
+
+@Composable
+internal fun MainBottomBar(
+ navigator: Navigator,
+ backStack: SaveableBackStack,
+ modifier: Modifier = Modifier,
+) {
+ val visible = shouldShowBottomBar(backStack)
+ val currentTab = getCurrentTab(backStack)
+ val tabs = MainTab.entries.toImmutableList()
+
+ MainBottomBar(
+ visible = visible,
+ tabs = tabs,
+ currentTab = currentTab,
+ onTabSelected = { tab ->
+ navigator.popUntilOrGoTo(tab.screen)
+ },
+ modifier = modifier,
+ )
+}
+
+@Composable
+internal fun MainBottomBar(
+ visible: Boolean,
+ tabs: ImmutableList,
+ currentTab: MainTab?,
+ onTabSelected: (MainTab) -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ AnimatedVisibility(
+ visible = visible,
+ enter = fadeIn() + slideIn { IntOffset(0, it.height) },
+ exit = fadeOut() + slideOut { IntOffset(0, it.height) },
+ modifier = modifier,
+ ) {
+ Box(modifier = Modifier.background(MaterialTheme.colorScheme.surface)) {
+ Column {
+ HorizontalDivider(color = MaterialTheme.colorScheme.outline)
+ Row(
+ modifier = Modifier
+ .navigationBarsPadding()
+ .fillMaxWidth()
+ .height(64.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ tabs.forEach { tab ->
+ MainBottomBarItem(
+ tab = tab,
+ selected = tab == currentTab,
+ onClick = {
+ if (tab != currentTab) {
+ onTabSelected(tab)
+ }
+ },
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun RowScope.MainBottomBarItem(
+ tab: MainTab,
+ selected: Boolean,
+ onClick: () -> Unit,
+) {
+ Box(
+ modifier = Modifier
+ .weight(1f)
+ .fillMaxHeight()
+ .selectable(
+ selected = selected,
+ indication = null,
+ role = null,
+ interactionSource = remember { MutableInteractionSource() },
+ onClick = onClick,
+ ),
+ contentAlignment = Alignment.Center,
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Icon(
+ imageVector = if (selected) ImageVector.vectorResource(tab.selectedIconResId)
+ else ImageVector.vectorResource(tab.iconResId),
+ contentDescription = tab.contentDescription,
+ tint = Color.Unspecified,
+ )
+ Spacer(modifier = Modifier.height(5.dp))
+ Text(
+ text = stringResource(tab.labelResId),
+ color = if (selected) Color(0xFF1F1F1F) else Color(0xFF9E9E9E),
+ fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal,
+ )
+ }
+ }
+}
+
+@ComponentPreview
+@Composable
+private fun MainBottomBarPreview() {
+ BooketTheme {
+ MainBottomBar(
+ visible = true,
+ tabs = MainTab.entries.toImmutableList(),
+ currentTab = MainTab.HOME,
+ onTabSelected = {},
+ )
+ }
+}
+
+fun Navigator.popUntilOrGoTo(screen: Screen) {
+ if (screen in peekBackStack()) {
+ popUntil { it == screen }
+ } else {
+ goTo(screen)
+ }
+}
+
+private val mainBottomBarScreens = setOf(HomeScreen, SearchScreen, LibraryScreen)
+
+@Composable
+private fun shouldShowBottomBar(backStack: SaveableBackStack): Boolean {
+ val currentScreen = backStack.topRecord?.screen
+ return currentScreen in mainBottomBarScreens
+}
+
+@Composable
+private fun getCurrentTab(backStack: SaveableBackStack): MainTab? {
+ val currentScreen = backStack.topRecord?.screen
+ return currentScreen?.let { screen ->
+ MainTab.entries.find { it.screen::class == currentScreen::class }
+ }
+}
diff --git a/feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/component/MainTab.kt b/feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/component/MainTab.kt
new file mode 100644
index 00000000..d6190034
--- /dev/null
+++ b/feature/main/src/main/kotlin/com/ninecraft/booket/feature/main/component/MainTab.kt
@@ -0,0 +1,39 @@
+package com.ninecraft.booket.feature.main.component
+
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
+import com.ninecraft.booket.feature.home.HomeScreen
+import com.ninecraft.booket.feature.library.LibraryScreen
+import com.ninecraft.booket.feature.main.R
+import com.ninecraft.booket.feature.search.SearchScreen
+import com.slack.circuit.runtime.screen.Screen
+
+internal enum class MainTab(
+ @DrawableRes val iconResId: Int,
+ @DrawableRes val selectedIconResId: Int,
+ @StringRes val labelResId: Int,
+ internal val contentDescription: String,
+ val screen: Screen,
+) {
+ HOME(
+ iconResId = R.drawable.ic_home,
+ selectedIconResId = R.drawable.ic_selected_home,
+ labelResId = R.string.home_label,
+ contentDescription = "Home Icon",
+ screen = HomeScreen,
+ ),
+ SEARCH(
+ iconResId = R.drawable.ic_search,
+ selectedIconResId = R.drawable.ic_selected_search,
+ labelResId = R.string.search_label,
+ contentDescription = "Search Icon",
+ screen = SearchScreen,
+ ),
+ LIBRARY(
+ iconResId = R.drawable.ic_library,
+ selectedIconResId = R.drawable.ic_selected_library,
+ labelResId = R.string.library_label,
+ contentDescription = "Library Icon",
+ screen = LibraryScreen,
+ ),
+}
diff --git a/feature/main/src/main/res/drawable/ic_home.xml b/feature/main/src/main/res/drawable/ic_home.xml
new file mode 100644
index 00000000..a3b9f8cc
--- /dev/null
+++ b/feature/main/src/main/res/drawable/ic_home.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/feature/main/src/main/res/drawable/ic_library.xml b/feature/main/src/main/res/drawable/ic_library.xml
new file mode 100644
index 00000000..b9ae331b
--- /dev/null
+++ b/feature/main/src/main/res/drawable/ic_library.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/feature/main/src/main/res/drawable/ic_search.xml b/feature/main/src/main/res/drawable/ic_search.xml
new file mode 100644
index 00000000..a64e320e
--- /dev/null
+++ b/feature/main/src/main/res/drawable/ic_search.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/feature/main/src/main/res/drawable/ic_selected_home.xml b/feature/main/src/main/res/drawable/ic_selected_home.xml
new file mode 100644
index 00000000..3e521bbd
--- /dev/null
+++ b/feature/main/src/main/res/drawable/ic_selected_home.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/feature/main/src/main/res/drawable/ic_selected_library.xml b/feature/main/src/main/res/drawable/ic_selected_library.xml
new file mode 100644
index 00000000..52c2a7af
--- /dev/null
+++ b/feature/main/src/main/res/drawable/ic_selected_library.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/feature/main/src/main/res/drawable/ic_selected_search.xml b/feature/main/src/main/res/drawable/ic_selected_search.xml
new file mode 100644
index 00000000..0812e9f5
--- /dev/null
+++ b/feature/main/src/main/res/drawable/ic_selected_search.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/feature/main/src/main/res/values/strings.xml b/feature/main/src/main/res/values/strings.xml
new file mode 100644
index 00000000..e3d059c3
--- /dev/null
+++ b/feature/main/src/main/res/values/strings.xml
@@ -0,0 +1,5 @@
+
+ 홈
+ 도서 검색
+ 내 서재
+
diff --git a/feature/search/.gitignore b/feature/search/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/feature/search/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/feature/search/build.gradle.kts b/feature/search/build.gradle.kts
new file mode 100644
index 00000000..33921e7f
--- /dev/null
+++ b/feature/search/build.gradle.kts
@@ -0,0 +1,25 @@
+@file:Suppress("INLINE_FROM_HIGHER_PLATFORM")
+
+plugins {
+ alias(libs.plugins.booket.android.feature)
+ alias(libs.plugins.kotlin.serialization)
+ alias(libs.plugins.kotlin.parcelize)
+}
+
+android {
+ namespace = "com.ninecraft.booket.feature.search"
+
+ buildFeatures {
+ buildConfig = true
+ }
+}
+
+ksp {
+ arg("circuit.codegen.mode", "hilt")
+}
+
+dependencies {
+ implementations(
+ libs.logger,
+ )
+}
diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchPresenter.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchPresenter.kt
new file mode 100644
index 00000000..be2cc2e1
--- /dev/null
+++ b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchPresenter.kt
@@ -0,0 +1,30 @@
+package com.ninecraft.booket.feature.search
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import com.slack.circuit.codegen.annotations.CircuitInject
+import com.slack.circuit.runtime.Navigator
+import com.slack.circuit.runtime.presenter.Presenter
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import dagger.hilt.android.components.ActivityRetainedComponent
+
+@Suppress("unused")
+class SearchPresenter @AssistedInject constructor(
+ @Assisted private val navigator: Navigator,
+) : Presenter {
+
+ @Composable
+ override fun present(): SearchScreen.State {
+ val scope = rememberCoroutineScope()
+
+ return SearchScreen.State {}
+ }
+
+ @CircuitInject(SearchScreen::class, ActivityRetainedComponent::class)
+ @AssistedFactory
+ fun interface Factory {
+ fun create(navigator: Navigator): SearchPresenter
+ }
+}
diff --git a/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchScreen.kt b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchScreen.kt
new file mode 100644
index 00000000..92b17575
--- /dev/null
+++ b/feature/search/src/main/kotlin/com/ninecraft/booket/feature/search/SearchScreen.kt
@@ -0,0 +1,65 @@
+package com.ninecraft.booket.feature.search
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import com.ninecraft.booket.core.designsystem.DevicePreview
+import com.ninecraft.booket.core.designsystem.theme.BooketTheme
+import com.slack.circuit.codegen.annotations.CircuitInject
+import com.slack.circuit.runtime.CircuitUiEvent
+import com.slack.circuit.runtime.CircuitUiState
+import com.slack.circuit.runtime.screen.Screen
+import dagger.hilt.android.components.ActivityRetainedComponent
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data object SearchScreen : Screen {
+ data class State(
+ val eventSink: (Event) -> Unit,
+ ) : CircuitUiState
+
+ sealed interface Event : CircuitUiEvent
+}
+
+@CircuitInject(SearchScreen::class, ActivityRetainedComponent::class)
+@Composable
+internal fun Search(
+ state: SearchScreen.State,
+ modifier: Modifier = Modifier,
+) {
+ Column(
+ modifier = modifier.fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ SearchContent(
+ state = state,
+ modifier = modifier,
+ )
+ }
+}
+
+@Suppress("unused")
+@Composable
+internal fun SearchContent(
+ state: SearchScreen.State,
+ modifier: Modifier = Modifier,
+) {
+ Text(text = "도서 검색")
+}
+
+@DevicePreview
+@Composable
+private fun SearchPreview() {
+ BooketTheme {
+ Search(
+ state = SearchScreen.State(
+ eventSink = {},
+ ),
+ )
+ }
+}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 6ec9ab6e..fbc130b5 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -35,6 +35,9 @@ hilt-navigation-compose = "1.2.0"
okhttp = "4.12.0"
retrofit = "2.11.0"
+## Circuit
+circuit = "0.28.1"
+
## Logging
logger = "2.2.0"
@@ -55,7 +58,8 @@ junit-version = "1.2.1"
[libraries]
android-gradle-plugin = { group = "com.android.tools.build", name = "gradle", version.ref = "android-gradle-plugin" }
-kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
+kotlin-gradle-plugin = { group = "org.jetbrains.kotlin", name="kotlin-gradle-plugin", version.ref = "kotlin" }
+compose-compiler-gradle-plugin = { group = "org.jetbrains.kotlin", name = "compose-compiler-gradle-plugin", version.ref = "kotlin" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" }
@@ -75,8 +79,6 @@ androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "androidx-compose-material3" }
-hilt-core = { group = "com.google.dagger", name = "hilt-core", version.ref = "hilt" }
-hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-android-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hilt-navigation-compose" }
@@ -88,9 +90,16 @@ okhttp-logging = { group = "com.squareup.okhttp3", name = "logging-interceptor",
kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" }
kotlinx-datetime = { group = "org.jetbrains.kotlinx", name = "kotlinx-datetime", version.ref = "kotlinx-datetime" }
-kotlinx-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "kotlinx-collections-immutable" }
+kotlinx-collections-immutable = { group = "org.jetbrains.kotlinx", name = "kotlinx-collections-immutable", version.ref = "kotlinx-collections-immutable" }
+
+circuit-foundation = { group = "com.slack.circuit", name = "circuit-foundation", version.ref = "circuit" }
+circuitx-android = { group = "com.slack.circuit", name = "circuitx-android", version.ref = "circuit" }
+circuitx-overlays = { group = "com.slack.circuit", name = "circuitx-overlays", version.ref = "circuit" }
+circuitx-gesture-navigation = { group = "com.slack.circuit", name = "circuitx-gesture-navigation", version.ref = "circuit" }
+circuit-codegen-annotation = { group = "com.slack.circuit", name = "circuit-codegen-annotations", version.ref = "circuit" }
+circuit-codegen-ksp = { group = "com.slack.circuit", name = "circuit-codegen", version.ref = "circuit" }
-orhanobut-logger = { module = "com.orhanobut:logger", version.ref = "logger" }
+logger = { group = "com.orhanobut", name = "logger", version.ref = "logger" }
detekt-formatting = { group = "io.gitlab.arturbosch.detekt", name = "detekt-formatting", version.ref = "kotlin-detekt" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
@@ -109,6 +118,7 @@ kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", versi
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "kotlin-detekt" }
kotlin-ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "kotlin-ktlint-gradle" }
+kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
@@ -131,3 +141,10 @@ androidx-compose = [
"androidx-compose-ui-graphics",
"androidx-compose-ui-tooling-preview",
]
+
+circuit = [
+ "circuit-foundation",
+ "circuitx-android",
+ "circuitx-overlays",
+ "circuitx-gesture-navigation",
+]
diff --git a/settings.gradle.kts b/settings.gradle.kts
index b53b27c3..8769ec32 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,4 +1,7 @@
+rootProject.name = "Booket-Android"
+
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
+enableFeaturePreview("STABLE_CONFIGURATION_CACHE")
pluginManagement {
includeBuild("build-logic")
@@ -17,5 +20,13 @@ dependencyResolutionManagement {
}
}
-rootProject.name = "Booket-Android"
-include(":app")
+include(
+ ":app",
+
+ ":core:designsystem",
+
+ ":feature:home",
+ ":feature:library",
+ ":feature:main",
+ ":feature:search",
+)