diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7beb525..cbb08ef 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -22,6 +22,14 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/angrypodo/wisp/navigation/Routes.kt b/app/src/main/java/com/angrypodo/wisp/navigation/Routes.kt
index fd49259..85e2284 100644
--- a/app/src/main/java/com/angrypodo/wisp/navigation/Routes.kt
+++ b/app/src/main/java/com/angrypodo/wisp/navigation/Routes.kt
@@ -8,13 +8,20 @@ import kotlinx.serialization.Serializable
data object Home
@Serializable
-@Wisp("product/{productId}")
-data class ProductDetail(val productId: String)
+@Wisp("product")
+data class ProductDetail(
+ val productId: Int,
+ val showReviews: Boolean = false
+)
@Serializable
@Wisp("settings")
data object Settings
+@Serializable
+@Wisp("user")
+data class UserRoute(val userId: Int)
+
@Serializable
@Wisp("splash")
data object Splash
diff --git a/app/src/main/java/com/angrypodo/wisp/ui/main/MainActivity.kt b/app/src/main/java/com/angrypodo/wisp/ui/main/MainActivity.kt
index 1d0e134..0d72656 100644
--- a/app/src/main/java/com/angrypodo/wisp/ui/main/MainActivity.kt
+++ b/app/src/main/java/com/angrypodo/wisp/ui/main/MainActivity.kt
@@ -18,11 +18,13 @@ import com.angrypodo.wisp.navigation.Home
import com.angrypodo.wisp.navigation.ProductDetail
import com.angrypodo.wisp.navigation.Settings
import com.angrypodo.wisp.navigation.Splash
+import com.angrypodo.wisp.navigation.UserRoute
import com.angrypodo.wisp.runtime.navigateTo
import com.angrypodo.wisp.ui.screens.HomeScreen
import com.angrypodo.wisp.ui.screens.ProductDetailScreen
import com.angrypodo.wisp.ui.screens.SettingsScreen
import com.angrypodo.wisp.ui.screens.SplashScreen
+import com.angrypodo.wisp.ui.screens.UserScreen
import com.angrypodo.wisp.ui.theme.WispTheme
class MainActivity : ComponentActivity() {
@@ -57,18 +59,29 @@ private fun WispNavHost(deepLinkUri: Uri?) {
composable {
HomeScreen(
onNavigateToProduct = {
- val uri = "app://wisp?stack=product/123|settings".toUri()
+ val uri = "app://wisp/product/settings?productId=123&showReviews=true".toUri()
navController.navigateTo(uri)
},
onNavigateToSettings = {
- val uri = "app://wisp?stack=settings".toUri()
+ val uri = "app://wisp/settings".toUri()
+ navController.navigateTo(uri)
+ },
+ onNavigateToMultiStack = {
+ val uri = "app://wisp/product/user?productId=123&userId=99".toUri()
navController.navigateTo(uri)
}
)
}
composable { backStackEntry ->
val productDetail: ProductDetail = backStackEntry.toRoute()
- ProductDetailScreen(productId = productDetail.productId)
+ ProductDetailScreen(
+ productId = productDetail.productId,
+ showReviews = productDetail.showReviews
+ )
+ }
+ composable { backStackEntry ->
+ val userRoute: UserRoute = backStackEntry.toRoute()
+ UserScreen(userId = userRoute.userId)
}
composable {
SettingsScreen()
diff --git a/app/src/main/java/com/angrypodo/wisp/ui/screens/SampleScreens.kt b/app/src/main/java/com/angrypodo/wisp/ui/screens/SampleScreens.kt
index 8edfc91..09611a2 100644
--- a/app/src/main/java/com/angrypodo/wisp/ui/screens/SampleScreens.kt
+++ b/app/src/main/java/com/angrypodo/wisp/ui/screens/SampleScreens.kt
@@ -20,7 +20,11 @@ import com.angrypodo.wisp.runtime.Wisp
import kotlinx.coroutines.delay
@Composable
-fun HomeScreen(onNavigateToProduct: () -> Unit, onNavigateToSettings: () -> Unit) {
+fun HomeScreen(
+ onNavigateToProduct: () -> Unit,
+ onNavigateToSettings: () -> Unit,
+ onNavigateToMultiStack: () -> Unit
+) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
@@ -29,17 +33,21 @@ fun HomeScreen(onNavigateToProduct: () -> Unit, onNavigateToSettings: () -> Unit
Text(text = "Home Screen", fontSize = 24.sp)
Spacer(modifier = Modifier.height(24.dp))
Button(onClick = onNavigateToProduct) {
- Text(text = "Go to Product 123 -> Settings (Deep Link)")
+ Text(text = "Go to Product 123 -> Settings")
}
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = onNavigateToSettings) {
- Text(text = "Go to Settings (Single Deep Link)")
+ Text(text = "Go to Settings")
+ }
+ Spacer(modifier = Modifier.height(16.dp))
+ Button(onClick = onNavigateToMultiStack) {
+ Text(text = "Product(123) -> User(99)")
}
}
}
@Composable
-fun ProductDetailScreen(productId: String) {
+fun ProductDetailScreen(productId: Int, showReviews: Boolean) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
@@ -48,6 +56,7 @@ fun ProductDetailScreen(productId: String) {
Text(text = "Product Detail Screen", fontSize = 24.sp)
Spacer(modifier = Modifier.height(16.dp))
Text(text = "Product ID: $productId", fontSize = 20.sp)
+ Text(text = "Show Reviews: $showReviews", fontSize = 16.sp)
}
}
@@ -62,6 +71,19 @@ fun SettingsScreen() {
}
}
+@Composable
+fun UserScreen(userId: Int) {
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(text = "User Screen", fontSize = 24.sp)
+ Spacer(modifier = Modifier.height(16.dp))
+ Text(text = "User ID: $userId", fontSize = 20.sp)
+ }
+}
+
@Composable
fun SplashScreen(
navController: NavController,
@@ -83,10 +105,8 @@ fun SplashScreen(
}
} else {
// Deep link found.
- // The first route is "splash", which we are already on.
- // We navigate to the rest of the stack.
- val routesToNavigate = routes.drop(1)
- wisp.navigateTo(navController, routesToNavigate)
+ // Navigate to the parsed stack.
+ wisp.navigateTo(navController, routes)
}
}
diff --git a/wisp-processor/src/main/java/com/angrypodo/wisp/generator/WispRegistryGenerator.kt b/wisp-processor/src/main/java/com/angrypodo/wisp/generator/WispRegistryGenerator.kt
index e5e8bc9..4e9a6a7 100644
--- a/wisp-processor/src/main/java/com/angrypodo/wisp/generator/WispRegistryGenerator.kt
+++ b/wisp-processor/src/main/java/com/angrypodo/wisp/generator/WispRegistryGenerator.kt
@@ -25,7 +25,7 @@ internal class WispRegistryGenerator {
val factoriesProperty = buildFactoriesProperty(routes)
- val registryObject = TypeSpec.objectBuilder(registryClassName)
+ val registryObject = TypeSpec.classBuilder(registryClassName)
.addSuperinterface(WispClassName.WISP_MODULE_REGISTRY) // Changed interface
.addModifiers(KModifier.PUBLIC)
.addProperty(factoriesProperty)
diff --git a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/Wisp.kt b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/Wisp.kt
index ca147c8..3369b68 100644
--- a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/Wisp.kt
+++ b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/Wisp.kt
@@ -24,16 +24,30 @@ class Wisp(
*/
fun resolveRoutes(uri: Uri): List {
val paths = parser.parse(uri)
+ val queryParams = getQueryParams(uri)
+
return paths.map { path ->
- matchAndCreate(path) ?: throw WispError.UnknownPath(path)
+ matchAndCreate(path, queryParams) ?: throw WispError.UnknownPath(path)
+ }
+ }
+
+ private fun getQueryParams(uri: Uri): Map {
+ val params = mutableMapOf()
+ uri.queryParameterNames.forEach { key ->
+ uri.getQueryParameter(key)?.let { value ->
+ params[key] = value
+ }
}
+ return params
}
- private fun matchAndCreate(path: String): Any? {
+ private fun matchAndCreate(path: String, queryParams: Map): Any? {
for ((pattern, factory) in mergedRoutes) {
- val params = WispUriMatcher.match(path, pattern)
- if (params != null) {
- return factory.create(params)
+ val pathVariables = WispUriMatcher.match(path, pattern)
+ if (pathVariables != null) {
+ // Path Variable과 Query Parameter 병합 (Query Param 우선순위 낮음)
+ val combinedParams = queryParams + pathVariables
+ return factory.create(combinedParams)
}
}
return null
@@ -71,7 +85,9 @@ class Wisp(
@JvmStatic
@Synchronized
- fun initialize() {
+ fun initialize(
+ parser: WispUriParser = DefaultWispUriParser()
+ ) {
if (instance == null) {
val aggregatedRoutes = mutableMapOf()
val loader = ServiceLoader.load(WispModuleRegistry::class.java)
@@ -80,7 +96,7 @@ class Wisp(
aggregatedRoutes.putAll(registry.getRoutes())
}
- instance = Wisp(aggregatedRoutes)
+ instance = Wisp(aggregatedRoutes, parser)
}
}
diff --git a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/parser/DefaultWispUriParser.kt b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/parser/DefaultWispUriParser.kt
index 0bf677b..0f25dae 100644
--- a/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/parser/DefaultWispUriParser.kt
+++ b/wisp-runtime/src/main/java/com/angrypodo/wisp/runtime/parser/DefaultWispUriParser.kt
@@ -1,26 +1,28 @@
package com.angrypodo.wisp.runtime.parser
import android.net.Uri
-import com.angrypodo.wisp.runtime.WispError
-import java.net.URLDecoder
-import java.nio.charset.StandardCharsets
-private const val STACK = "stack"
+/**
+ * 기본 URI 파서 구현체입니다.
+ * URI의 경로(Path)를 지정된 구분자([delimiter])로 분리하여 라우트 경로 리스트를 생성합니다.
+ *
+ * 예: "myapp://home/product" (delimiter="/") -> ["home", "product"]
+ */
+class DefaultWispUriParser(
+ private val delimiter: String = "/"
+) : WispUriParser {
-class DefaultWispUriParser : WispUriParser {
override fun parse(uri: Uri): List {
- val encodedStack = uri.getQueryParameter(STACK)
+ val path = uri.path ?: return emptyList()
- if (encodedStack.isNullOrBlank()) {
- throw WispError.ParsingFailed(uri.toString(), "Missing 'stack' query parameter")
+ // 경로가 구분자로 시작하면 제거 (예: "/home" -> "home")
+ val trimmedPath = if (path.startsWith(delimiter)) {
+ path.substring(delimiter.length)
+ } else {
+ path
}
- return try {
- val decodedStack = URLDecoder.decode(encodedStack, StandardCharsets.UTF_8.name())
-
- decodedStack.split("|").filter { it.isNotBlank() }
- } catch (e: Exception) {
- throw WispError.ParsingFailed(uri.toString(), e.message ?: "Unknown decoding error")
- }
+ return trimmedPath.split(delimiter)
+ .filter { it.isNotEmpty() }
}
}