Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 53 additions & 5 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ plugins {
alias(libs.plugins.hilt)
alias(libs.plugins.ksp)
alias(libs.plugins.detekt)
alias(libs.plugins.kotlin.serialization)
jacoco
}

Expand Down Expand Up @@ -134,8 +135,10 @@ dependencies {
implementation(project(":data:product"))
implementation(project(":core:designsystem"))
implementation(project(":feature:catalog"))
implementation(project(":feature:detail"))
implementation(libs.androidx.navigation.compose)
implementation(libs.hilt.navigation.compose)
implementation(libs.kotlinx.serialization.json)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
Expand Down Expand Up @@ -170,7 +173,8 @@ tasks.register<JacocoReport>("jacocoTestReport") {
":domain:product:test",
":core:common:test",
":data:product:testDebugUnitTest",
":feature:catalog:testDebugUnitTest"
":feature:catalog:testDebugUnitTest",
":feature:detail:testDebugUnitTest"
)

reports {
Expand Down Expand Up @@ -228,17 +232,32 @@ tasks.register<JacocoReport>("jacocoTestReport") {
exclude(fileFilter)
}

val detailTree = fileTree(project(":feature:detail").layout.buildDirectory.get()) {
include("intermediates/built_in_kotlinc/debug/compileDebugKotlin/classes/**/*.class")
exclude(fileFilter)
}

sourceDirectories.setFrom(
files(
"src/main/java",
"${project(":domain:product").projectDir}/src/main/java",
"${project(":core:common").projectDir}/src/main/java",
"${project(":data:product").projectDir}/src/main/java",
"${project(":feature:catalog").projectDir}/src/main/java"
"${project(":feature:catalog").projectDir}/src/main/java",
"${project(":feature:detail").projectDir}/src/main/java"
)
)

classDirectories.setFrom(files(appTree, domainTree, coreCommonTree, dataTree, catalogTree))
classDirectories.setFrom(
files(
appTree,
domainTree,
coreCommonTree,
dataTree,
catalogTree,
detailTree
)
)

executionData.setFrom(
files(
Expand All @@ -256,6 +275,9 @@ tasks.register<JacocoReport>("jacocoTestReport") {
},
fileTree(project(":feature:catalog").layout.buildDirectory.get()) {
include("jacoco/testDebugUnitTest.exec")
},
fileTree(project(":feature:detail").layout.buildDirectory.get()) {
include("jacoco/testDebugUnitTest.exec")
}
)
)
Expand Down Expand Up @@ -307,6 +329,14 @@ tasks.register<JacocoCoverageVerification>("jacocoTestCoverageVerification") {
minimum = "0.75".toBigDecimal()
}
}

rule {
element = "CLASS"
includes = listOf("com.example.catalogapp.feature.detail.*ViewModel*")
limit {
minimum = "0.75".toBigDecimal()
}
}
}

val fileFilter = listOf(
Expand Down Expand Up @@ -360,17 +390,32 @@ tasks.register<JacocoCoverageVerification>("jacocoTestCoverageVerification") {
exclude(fileFilter)
}

val detailTree = fileTree(project(":feature:detail").layout.buildDirectory.get()) {
include("intermediates/built_in_kotlinc/debug/compileDebugKotlin/classes/**/*.class")
exclude(fileFilter)
}

sourceDirectories.setFrom(
files(
"src/main/java",
"${project(":domain:product").projectDir}/src/main/java",
"${project(":core:common").projectDir}/src/main/java",
"${project(":data:product").projectDir}/src/main/java",
"${project(":feature:catalog").projectDir}/src/main/java"
"${project(":feature:catalog").projectDir}/src/main/java",
"${project(":feature:detail").projectDir}/src/main/java"
)
)

classDirectories.setFrom(files(appTree, domainTree, coreCommonTree, dataTree, catalogTree))
classDirectories.setFrom(
files(
appTree,
domainTree,
coreCommonTree,
dataTree,
catalogTree,
detailTree
)
)

executionData.setFrom(
files(
Expand All @@ -388,6 +433,9 @@ tasks.register<JacocoCoverageVerification>("jacocoTestCoverageVerification") {
},
fileTree(project(":feature:catalog").layout.buildDirectory.get()) {
include("jacoco/testDebugUnitTest.exec")
},
fileTree(project(":feature:detail").layout.buildDirectory.get()) {
include("jacoco/testDebugUnitTest.exec")
}
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,34 @@ import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import com.example.catalogapp.feature.catalog.CatalogScreen
import com.example.catalogapp.feature.detail.DetailScreen

@Composable
fun CatalogNavHost(
navController: NavHostController = rememberNavController()
) {
NavHost(
navController = navController,
startDestination = Screen.Catalog.route
startDestination = CatalogRoute
) {
composable(Screen.Catalog.route) {
composable<CatalogRoute> {
CatalogScreen(
onNavigateToDetail = { productId ->
navController.navigate(Screen.Detail.createRoute(productId))
navController.navigate(DetailRoute(productId))
}
)
}

// Detail screen — added in next session
composable(Screen.Detail.route) {
// Placeholder until feature:detail is built
composable<DetailRoute> { backStackEntry ->
val route = backStackEntry.toRoute<DetailRoute>()
DetailScreen(
productId = route.productId,
onNavigateBack = { navController.popBackStack() }
)
}
}
}


14 changes: 14 additions & 0 deletions app/src/main/java/com/example/catalogapp/navigation/Routes.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example.catalogapp.navigation

import kotlinx.serialization.Serializable

// ─── Type-safe route definitions ──────────────────────────────────────────────
// Each route is a @Serializable class/object — Navigation generates
// the route string automatically, no manual string formatting needed.
// Compile-time safety: wrong argument types fail at build time, not runtime.

@Serializable
object CatalogRoute

@Serializable
data class DetailRoute(val productId: Int)
8 changes: 0 additions & 8 deletions app/src/main/java/com/example/catalogapp/navigation/Screen.kt

This file was deleted.

1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ plugins {
alias(libs.plugins.hilt) apply false
alias(libs.plugins.ksp) apply false
alias(libs.plugins.detekt) apply false
alias(libs.plugins.kotlin.serialization) apply false
}
1 change: 1 addition & 0 deletions feature/detail/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
61 changes: 61 additions & 0 deletions feature/detail/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.hilt)
alias(libs.plugins.ksp)
alias(libs.plugins.detekt)
jacoco
}

android {
namespace = "com.example.catalogapp.feature.detail"
compileSdk = 36

defaultConfig {
minSdk = 24
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

buildFeatures {
compose = true
}
}

jacoco {
toolVersion = "0.8.12"
}

detekt {
config.setFrom(files("$rootDir/config/detekt/detekt.yml"))
buildUponDefaultConfig = true
source.setFrom("src/main/java")
}

dependencies {
implementation(project(":domain:product"))
implementation(project(":core:common"))
implementation(project(":core:designsystem"))

implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.compose.foundation)
implementation(libs.androidx.compose.material.icons.extended)
debugImplementation(libs.androidx.compose.ui.tooling)

implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
implementation(libs.hilt.navigation.compose)
implementation(libs.kotlinx.coroutines.android)
implementation(libs.coil.compose)

testImplementation(libs.junit)
testImplementation(libs.mockk)
testImplementation(libs.kotlinx.coroutines.test)
}
Empty file.
21 changes: 21 additions & 0 deletions feature/detail/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.example.catalogapp.feature.detail

import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4

import org.junit.Test
import org.junit.runner.RunWith

import org.junit.Assert.*

/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.example.catalogapp.feature.detail.test", appContext.packageName)
}
}
4 changes: 4 additions & 0 deletions feature/detail/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.example.catalogapp.feature.detail

import com.example.catalogapp.domain.product.Product

// ─── UI State ─────────────────────────────────────────────────────────────────
data class DetailUiState(
val product: Product? = null,
val isLoading: Boolean = false,
val error: String? = null,
val isWishlisted: Boolean = false,
val isDescriptionExpanded: Boolean = false
) {
val showLoading: Boolean get() = isLoading && product == null
val showError: Boolean get() = error != null && !isLoading && product == null
val showContent: Boolean get() = product != null
}

// ─── Intent ───────────────────────────────────────────────────────────────────
sealed class DetailIntent {
data class LoadProduct(val productId: Int) : DetailIntent()
object NavigateBack : DetailIntent()
object ToggleWishlist : DetailIntent()
object AddToCart : DetailIntent()
object ToggleDescription : DetailIntent()
object ShareProduct : DetailIntent()
}

// ─── Effect ───────────────────────────────────────────────────────────────────
sealed class DetailEffect {
object NavigateBack : DetailEffect()
data class ShowSnackbar(val message: String) : DetailEffect()
data class ShareProduct(val title: String, val price: Double) : DetailEffect()
}
Loading
Loading