diff --git a/README.ko.md b/README.ko.md index 8295bea..0725746 100644 --- a/README.ko.md +++ b/README.ko.md @@ -1,11 +1,22 @@ # Wisp -[![CI](https://github.com/angrypodo/wisp/actions/workflows/ci.yml/badge.svg)](https://github.com/angrypodo/wisp/actions/workflows/ci.yml) - -**Wisp**는 Jetpack Compose를 위한 타입 세이프(type-safe), 서버 주도(server-driven) 딥링크 라이브러리입니다. 단일 표준 URI를 기반으로 내비게이션 백스택을 동적으로 구성할 수 있게 하여, 표준 `navigation-compose` 라이브러리의 정적 백스택 한계를 극복합니다. +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) +[![API](https://img.shields.io/badge/API-21%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=21) +[![Build Status](https://github.com/angrypodo/wisp/actions/workflows/ci.yml/badge.svg)](https://github.com/angrypodo/wisp/actions/workflows/ci.yml) +[![Maven Central](https://img.shields.io/maven-central/v/io.github.angrypodo/wisp-runtime)](https://central.sonatype.com/artifact/io.github.angrypodo/wisp-runtime) 번역: [Read in English](README.md) +
+ +

+ Wisp는 Jetpack Compose를 위한 타입 세이프(type-safe), 서버 주도(server-driven) 딥링크 라이브러리입니다.
+ 단일 표준 URI를 기반으로 내비게이션 백스택을 동적으로 구성할 수 있게 하여,
+ 표준 navigation-compose 라이브러리의 정적 백스택 한계를 극복합니다. +

+ +
+ ## 🤔 Wisp, 왜 필요한가요? Jetpack Compose의 표준 딥링크 기능은 미리 정의된 정적 백스택을 만드는 데 주로 사용됩니다. 이 때문에 서버가 실시간으로 동적인 사용자 여정(예: `상품 화면 -> 쿠폰 화면 -> 결제 화면`)을 제어해야 하는 시나리오를 구현하기는 어렵습니다. @@ -14,17 +25,64 @@ Wisp는 URI의 경로 세그먼트(path segments)로부터 전체 백스택을 ## 🏛️ 아키텍처 및 요구사항 -- **싱글 액티비티 아키텍처 (Single-Activity Architecture):** Wisp는 **싱글 액티비티 구조** 전용으로 설계되었으며, 여러 Activity 간의 내비게이션은 지원하지 않습니다. 이는 Jetpack Compose에 권장되는 최신 안드로이드 개발 방식과 일치합니다. -- **Jetpack Navigation 및 타입 안정성:** 이 라이브러리는 Jetpack Navigation Compose의 확장 기능이며, **타입 세이프(type-safe) 내비게이션** 패러다임 전용으로 설계되었습니다. `NavController`가 반드시 필요하며, 전통적인 문자열 기반의 라우트는 지원하지 않습니다. -- **멀티 모듈 지원 (Multi-Module Support):** Wisp는 멀티 모듈 프로젝트를 완벽하게 지원합니다. `ServiceLoader` 패턴을 사용하여, 라이브러리가 포함된 모든 모듈로부터 `@Wisp` 라우트 정의를 자동으로 탐지합니다. +- **싱글 액티비티 아키텍처:** Wisp는 **싱글 액티비티 구조(Single-Activity Architecture)**를 위해 설계되었으며, 여러 Activity 간의 내비게이션은 지원하지 않습니다. 이는 Jetpack Compose에 권장되는 최신 안드로이드 개발 방식과 일치합니다. +- **Jetpack Navigation 및 타입 안정성:** 이 라이브러리는 Jetpack Navigation Compose의 **타입 세이프(type-safe) 내비게이션** 패러다임 전용으로 설계되었습니다. `NavController`가 반드시 필요하며, 전통적인 문자열 기반의 라우트는 지원하지 않습니다. +- **멀티 모듈 지원:** Wisp는 멀티 모듈 프로젝트를 완벽하게 지원합니다. `ServiceLoader` 패턴을 사용하여, 라이브러리가 포함된 모든 모듈로부터 `@Wisp` 라우트 정의를 자동으로 탐지합니다. +- **최소 요구사항:** + - **minSdk:** 21 (Android 5.0) + - **Kotlin:** 1.9.0 이상 (KSP 호환 버전) + +## 다운로드 (Download) -## 🚀 사용법 +[![Maven Central](https://img.shields.io/maven-central/v/io.github.angrypodo/wisp-runtime)](https://central.sonatype.com/artifact/io.github.angrypodo/wisp-runtime) -**참고:** Wisp는 아직 Maven Central에 배포되지 않았습니다. 현재로서는 이 리포지토리를 클론하여 프로젝트에 로컬 모듈로 포함해야 합니다. +### Version Catalog + +Version Catalog를 사용 중이라면, `libs.versions.toml` 파일에 다음과 같이 의존성을 추가할 수 있습니다: + +```toml +[versions] +#... +wisp = "0.1.0" + +[libraries] +#... +wisp-runtime = { module = "io.github.angrypodo:wisp-runtime", version.ref = "wisp" } +wisp-processor = { module = "io.github.angrypodo:wisp-processor", version.ref = "wisp" } +``` + +### Gradle + +프로젝트 수준의 `build.gradle.kts` 파일에 KSP 플러그인을 추가합니다. **반드시 사용하는 Kotlin 버전과 호환되는 KSP 버전을 사용하세요.** ([KSP 릴리즈 확인](https://github.com/google/ksp/releases)) + +```kotlin +plugins { + id("com.google.devtools.ksp") version "YOUR_KSP_VERSION" apply false +} +``` + +그리고 **모듈** 수준의 `build.gradle.kts` 파일에 의존성을 추가합니다: + +```kotlin +plugins { + id("com.google.devtools.ksp") +} + +dependencies { + implementation("io.github.angrypodo:wisp-runtime:0.1.0") + ksp("io.github.angrypodo:wisp-processor:0.1.0") + + // Version Catalog를 사용하는 경우 + // implementation(libs.wisp.runtime) + // ksp(libs.wisp.processor) +} +``` + +## 사용법 (Usage) ### 1. 라우트 정의하기 -`@Serializable` 어노테이션이 달린 `data class`나 `object`에 `@Wisp` 어노테이션을 추가하여 딥링크 대상을 지정합니다. `@Wisp`에 전달하는 문자열은 딥링크 URI에서 사용될 경로 세그먼트가 됩니다. +`@Serializable` 어노테이션이 달린 `data class`나 `object`에 `@Wisp` 어노테이션을 추가하여 딥링크 대상을 지정합니다. 라우트 클래스의 속성들은 URI의 **쿼리 파라미터**로부터 자동으로 값이 채워집니다. 만약 속성에 **기본값(default value)**이 있다면, 해당 속성은 선택적(optional)인 값이 됩니다. @@ -34,7 +92,7 @@ import com.angrypodo.wisp.annotations.Wisp import kotlinx.serialization.Serializable @Serializable -@Wisp("product") // "product" 경로와 매칭 +@Wisp("product") // "product" 경로 세그먼트와 매칭 data class ProductDetail( val productId: Int, // "?productId=..." 로부터 값을 받음 val showReviews: Boolean = false // 선택적. "?showReviews=..." 값이 없으면 false 사용 @@ -91,23 +149,7 @@ val uri = "app://wisp/product/user?productId=123&userId=99".toUri() navController.navigateTo(uri) ``` -## 🧪 테스트 방법 - -### 샘플 앱 실행하기 - -1. 이 리포지토리를 클론하여 Android Studio에서 엽니다. -2. `app` 실행 구성을 선택하고 에뮬레이터나 실제 기기에서 실행합니다. -3. 앱 내의 버튼을 눌러 내비게이션을 테스트합니다. - -### ADB로 테스트하기 - -`adb`를 사용하여 커맨드 라인에서 직접 딥링크를 테스트할 수 있습니다. 이는 외부 소스로부터의 링크 클릭을 시뮬레이션하는 좋은 방법입니다. - -```bash -adb shell am start -a android.intent.action.VIEW -d "app://wisp/product/user?productId=123&userId=99" -``` - -## 고급 사용법 +## 고급 사용법 (Advanced Usage) ### 커스텀 URI 파서 @@ -124,9 +166,9 @@ Wisp.initialize(parser = myParser) - **Kotlinx Serialization:** Wisp는 쿼리 파라미터를 라우트 데이터 클래스로 역직렬화하기 위해 `kotlinx.serialization`에 크게 의존합니다. - **파라미터 이름:** URI의 쿼리 파라미터 키는 라우트 `data class`의 속성 이름과 정확히 일치해야 합니다. -## 📜 라이선스 +# License -``` +```xml Copyright 2025 angrypodo Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/README.md b/README.md index 210b278..6bee796 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,22 @@ # Wisp -[![CI](https://github.com/angrypodo/wisp/actions/workflows/ci.yml/badge.svg)](https://github.com/angrypodo/wisp/actions/workflows/ci.yml) - -**Wisp** is a type-safe, server-driven deep link library for Jetpack Compose. It allows you to dynamically build your navigation backstack from a single, standard URI, overcoming the static backstack limitations of the `navigation-compose` library. +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) +[![API](https://img.shields.io/badge/API-21%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=21) +[![Build Status](https://github.com/angrypodo/wisp/actions/workflows/ci.yml/badge.svg)](https://github.com/angrypodo/wisp/actions/workflows/ci.yml) +[![Maven Central](https://img.shields.io/maven-central/v/io.github.angrypodo/wisp-runtime)](https://central.sonatype.com/artifact/io.github.angrypodo/wisp-runtime) translation: [Read in Korean (한국어)](./README.ko.md) +
+ +

+ Wisp is a type-safe, server-driven deep link library for Jetpack Compose.
+ It allows you to dynamically build your navigation backstack from a single, standard URI,
+ overcoming the static backstack limitations of the navigation-compose library. +

+ +
+ ## 🤔 Why Wisp? Standard deep links in Jetpack Compose often lead to predefined, static backstacks. It's challenging to implement scenarios where a server needs to dictate a dynamic user journey on the fly (e.g., `Product Screen -> Coupon Screen -> Checkout Screen`). @@ -14,17 +25,64 @@ Wisp automates this process by building the entire backstack from the URI's path ## 🏛️ Architecture & Prerequisites -- **Single-Activity Architecture:** Wisp is designed for a **Single-Activity Architecture** and does not support navigating between different Activities. This aligns with the modern Android development practices recommended for Jetpack Compose. -- **Jetpack Navigation & Type-Safety:** The library is an extension of Jetpack Navigation Compose and is exclusively designed for its **type-safe navigation** paradigm. It requires a `NavController` and does not support traditional string-based routes. -- **Multi-Module Support:** Wisp fully supports multi-module projects. It automatically discovers `@Wisp` route definitions from all modules that include the library, using a `ServiceLoader` pattern. +- **Single-Activity Architecture:** Wisp is designed for a **Single-Activity Architecture** and does not support navigating between different Activities. +- **Jetpack Navigation & Type-Safety:** The library is exclusively designed for the **type-safe navigation** paradigm of Jetpack Navigation Compose. It requires a `NavController` and does not support traditional string-based routes. +- **Multi-Module Support:** Wisp fully supports multi-module projects using a `ServiceLoader` pattern. +- **Minimum Requirements:** + - **minSdk:** 21 (Android 5.0) + - **Kotlin:** 1.9.0 or higher (Compatible with KSP) + +## Download -## 🚀 How to Use +[![Maven Central](https://img.shields.io/maven-central/v/io.github.angrypodo/wisp-runtime)](https://central.sonatype.com/artifact/io.github.angrypodo/wisp-runtime) -**Note:** Wisp is not yet published to Maven Central. To use it, you currently need to clone this repository and include the modules in your project locally. +### Version Catalog + +If you're using Version Catalog, you can configure the dependency by adding it to your `libs.versions.toml` file as follows: + +```toml +[versions] +#... +wisp = "0.1.0" + +[libraries] +#... +wisp-runtime = { module = "io.github.angrypodo:wisp-runtime", version.ref = "wisp" } +wisp-processor = { module = "io.github.angrypodo:wisp-processor", version.ref = "wisp" } +``` + +### Gradle + +Add the KSP plugin to your project-level `build.gradle.kts`. **Make sure to use a KSP version that matches your Kotlin version.** (Check [KSP Releases](https://github.com/google/ksp/releases)) + +```kotlin +plugins { + id("com.google.devtools.ksp") version "YOUR_KSP_VERSION" apply false +} +``` + +Then, add the dependencies to your **module**'s `build.gradle.kts` file: + +```kotlin +plugins { + id("com.google.devtools.ksp") +} + +dependencies { + implementation("io.github.angrypodo:wisp-runtime:0.1.0") + ksp("io.github.angrypodo:wisp-processor:0.1.0") + + // if you're using Version Catalog + // implementation(libs.wisp.runtime) + // ksp(libs.wisp.processor) +} +``` + +## Usage ### 1. Define Routes -Designate a deep link destination by adding the `@Wisp` annotation to any `@Serializable` `data class` or `object`. The string passed to `@Wisp` is the path segment that will be used in the deep link URI. +Designate a deep link destination by adding the `@Wisp` annotation to any `@Serializable` `data class` or `object`. Route properties are automatically populated from the URI's **query parameters**. If a property has a **default value**, it is considered optional. @@ -43,7 +101,7 @@ data class ProductDetail( ### 2. Configure the Manifest -For deep links to be accessible from outside your app, you must register an `` in your `AndroidManifest.xml`. Both `scheme` and `host` are required. +Register an `` in your `AndroidManifest.xml`. Both `scheme` and `host` are required. ```xml @@ -92,22 +150,6 @@ val uri = "app://wisp/product/user?productId=123&userId=99".toUri() navController.navigateTo(uri) ``` -## 🧪 Testing - -### Running the Sample App - -1. Clone this repository and open it in Android Studio. -2. Select the `app` run configuration and run it on an emulator or a physical device. -3. Use the buttons in the app to test navigation. - -### Testing with ADB - -You can test your deep links directly from the command line using `adb`. This is a great way to simulate a link click from an external source. - -```bash -adb shell am start -a android.intent.action.VIEW -d "app://wisp/product/user?productId=123&userId=99" -``` - ## Advanced Usage ### Custom URI Parser @@ -125,9 +167,9 @@ Wisp.initialize(parser = myParser) - **Kotlinx Serialization:** Wisp relies heavily on `kotlinx.serialization` to deserialize query parameters into your route data classes. - **Parameter Naming:** The query parameter keys in the URI must exactly match the property names in your route `data class`. -## 📜 License +# License -``` +```xml Copyright 2025 angrypodo Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ec69797..0246b14 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -43,10 +43,6 @@ kotlin { } } -tasks.withType { - useJUnitPlatform() -} - dependencies { implementation(project(":wisp-runtime")) ksp(project(":wisp-processor")) diff --git a/build.gradle.kts b/build.gradle.kts index b051aff..41aa8a7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,14 +1,20 @@ import org.jlleitschuh.gradle.ktlint.KtlintExtension plugins { + // Android alias(libs.plugins.android.application) apply false alias(libs.plugins.android.library) apply false + + // Kotlin alias(libs.plugins.kotlin.android) apply false alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.kotlin.compose) apply false - alias(libs.plugins.ksp) apply false alias(libs.plugins.kotlin.serialization) apply false + + // Tools + alias(libs.plugins.ksp) apply false alias(libs.plugins.ktlint) apply false + alias(libs.plugins.vanniktech.maven.publish) apply false } subprojects { @@ -21,4 +27,45 @@ subprojects { verbose.set(true) outputToConsole.set(true) } + + tasks.withType { + useJUnitPlatform() + } + + if (name != "app") { + apply(plugin = "com.vanniktech.maven.publish") + + extensions.configure { + publishToMavenCentral(com.vanniktech.maven.publish.SonatypeHost.CENTRAL_PORTAL) + signAllPublications() + + coordinates("io.github.angrypodo", name, "0.1.0") + + pom { + name.set(project.name) + description.set("Wisp library: ${project.name}") + inceptionYear.set("2025") + url.set("https://github.com/angrypodo/wisp") + licenses { + license { + name.set("The Apache License, Version 2.0") + url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") + distribution.set("http://www.apache.org/licenses/LICENSE-2.0.txt") + } + } + developers { + developer { + id.set("angrypodo") + name.set("MinJae Han") + url.set("https://github.com/angrypodo") + } + } + scm { + url.set("https://github.com/angrypodo/wisp") + connection.set("scm:git:git://github.com/angrypodo/wisp.git") + developerConnection.set("scm:git:ssh://git@github.com/angrypodo/wisp.git") + } + } + } + } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5c69935..86ec710 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,50 +1,52 @@ [versions] -# Build Configuration +# Build & Publish androidGradlePlugin = "8.13.0" -compileSdk = "36" -minSdk = "28" -jvmTarget = "11" +vanniktechMavenPublish = "0.30.0" +ksp = "2.3.0" +ktlint = "11.5.1" # Kotlin kotlin = "2.2.21" -ksp = "2.3.0" kotlinxSerialization = "1.9.0" kotlinpoet = "2.2.0" -# AndroidX +# Android SDK +compileSdk = "36" +minSdk = "28" +jvmTarget = "11" + +# AndroidX & Compose coreKtx = "1.17.0" appcompat = "1.7.1" activityCompose = "1.11.0" composeBom = "2025.10.01" composeNavigation = "2.9.5" +material = "1.13.0" # Test junit = "6.0.1" junitVersion = "1.3.0" espressoCore = "3.7.0" -# Third Party -material = "1.13.0" -ktlint = "11.5.1" - [libraries] -# Build Tools +# --- Build Tools & Plugins --- android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" } kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } +vanniktech-maven-publish = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "vanniktechMavenPublish" } -# Code Generation +# --- Code Generation --- ksp-api = { group = "com.google.devtools.ksp", name = "symbol-processing-api", version.ref = "ksp" } kotlinpoet-ksp = { group = "com.squareup", name = "kotlinpoet-ksp", version.ref = "kotlinpoet" } -# Kotlin & Coroutines +# --- Kotlin & Libraries --- kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerialization" } -# AndroidX Core +# --- AndroidX Core & UI --- androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } -# AndroidX Compose +# --- AndroidX Compose --- androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } @@ -55,7 +57,7 @@ androidx-compose-material3 = { group = "androidx.compose.material3", name = "mat androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "composeNavigation" } androidx-navigation-testing = { group = "androidx.navigation", name = "navigation-testing", version.ref = "composeNavigation" } -# Test +# --- Test --- junit-bom = { group = "org.junit", name = "junit-bom", version.ref = "junit" } junit-jupiter-api = { group = "org.junit.jupiter", name = "junit-jupiter-api" } junit-jupiter-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine" } @@ -76,8 +78,7 @@ kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } -# KSP +# Tools ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } - -# Lint -ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } \ No newline at end of file +ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } +vanniktech-maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktechMavenPublish" } diff --git a/wisp-processor/build.gradle.kts b/wisp-processor/build.gradle.kts index a8bb046..2b4608d 100644 --- a/wisp-processor/build.gradle.kts +++ b/wisp-processor/build.gradle.kts @@ -12,7 +12,3 @@ dependencies { testRuntimeOnly(libs.junit.jupiter.engine) testRuntimeOnly(libs.junit.platform.launcher) } - -tasks.withType { - useJUnitPlatform() -} diff --git a/wisp-runtime/build.gradle.kts b/wisp-runtime/build.gradle.kts index 7cf1463..0323544 100644 --- a/wisp-runtime/build.gradle.kts +++ b/wisp-runtime/build.gradle.kts @@ -38,10 +38,6 @@ kotlin { } } -tasks.withType { - useJUnitPlatform() -} - dependencies { api(project(":wisp-annotations"))