translation: Read in Korean (νκ΅μ΄)
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.
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).
Wisp automates this process by building the entire backstack from the URI's path segments. It uses annotation processing (KSP) to generate the necessary boilerplate, allowing you to focus solely on defining your routes.
- 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
NavControllerand does not support traditional string-based routes. - Multi-Module Support: Wisp fully supports multi-module projects using a
ServiceLoaderpattern. - Minimum Requirements:
- minSdk: 21 (Android 5.0)
- Kotlin: 1.9.0 or higher (Compatible with KSP)
If you're using Version Catalog, you can configure the dependency by adding it to your libs.versions.toml file as follows:
[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" }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)
plugins {
id("com.google.devtools.ksp") version "YOUR_KSP_VERSION" apply false
}Then, add the dependencies to your module's build.gradle.kts file:
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)
}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.
// In your navigation or feature module
import com.angrypodo.wisp.annotations.Wisp
import kotlinx.serialization.Serializable
@Serializable
@Wisp("product") // Matches path segment "product"
data class ProductDetail(
val productId: Int, // Populated from "?productId=..."
val showReviews: Boolean = false // Optional, populated from "?showReviews=..."
)Register an <intent-filter> in your AndroidManifest.xml. Both scheme and host are required.
<!-- In AndroidManifest.xml -->
<activity ... >
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="app" android:host="wisp" />
</intent-filter>
</activity>In your Application class, call Wisp.initialize().
// In your app's Application class
import android.app.Application
import com.angrypodo.wisp.runtime.Wisp
class SampleApplication : Application() {
override fun onCreate() {
super.onCreate()
// Initialize Wisp. It will automatically find all route registries.
Wisp.initialize()
}
}Note: Don't forget to add
android:name=".SampleApplication"to the<application>tag in yourAndroidManifest.xml.
Construct a deep link URI and use the navigateTo extension function on your NavController.
- URI Format:
scheme://host/path1/path2?paramKey=paramValue - Backstack: The backstack is built from the URI's path segments.
- Parameters: Route properties are populated from the URI's query parameters.
// This URI creates a backstack: ProductDetail -> UserRoute
// - ProductDetail gets productId=123. 'showReviews' uses its default value (false).
// - UserRoute gets userId=99
val uri = "app://wisp/product/user?productId=123&userId=99".toUri()
navController.navigateTo(uri)- Clone this repository and open it in Android Studio.
- Select the
apprun configuration and run it on an emulator or a physical device. - Use the buttons in the app to test navigation.
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.
Important: When testing multiple parameters on the command line, you must escape the & character (\&) or wrap the entire URI in single quotes to prevent the shell from interpreting it as a background command.
# Escape the '&' character with a backslash
adb shell am start -a android.intent.action.VIEW -d "app://wisp/product/user?productId=123\&userId=99"By default, Wisp parses the backstack from the URI path by splitting it with a / delimiter. If your deep link scheme requires a different logic (e.g., using | as a delimiter), you can provide your own implementation of the WispUriParser interface.
val myParser = DefaultWispUriParser(delimiter = "|")
Wisp.initialize(parser = myParser)- Parameter Source: Route parameters are populated exclusively from URI query parameters. The path is used only for defining the backstack sequence.
- Kotlinx Serialization: Wisp relies heavily on
kotlinx.serializationto 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.
Copyright 2025 angrypodo
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.