KeepLink

Kotlin multiplatform deep-link definition, parsing and creation library.
Features:
- Create sealed deep-link hierarchy to exactly define each link once for server and client
- Build, parse and route in a type-safe way
- Code completion on all platforms
- Auto-generating documentation always in-line with the implementation
API documentation is available at Github Pages folder.
Contents
Motivation
- As a business user I want an application to perform a certain action (open a page, update some data) by clicking
a hyper-text link on a web page or in a message.
Proposed deep-link management flow
- Business evaluates the new feature and decides that it's good to have a deep-link to it.
- The web and mobile teams implement the feature
- The web team reports a link on the product site to become a deep-link to be opened in application
- The deep-link supporting team implements the new
Action definition in the link registry.
- The new version of the deep-link library is distributed among teams.
- Mobile teams adjust their routers to support the link.
- Backend programmers build links with the library in backend to frontend data export.
Link structure
Application deep-links are URIs:
URI = scheme:[host]path[?search][#hash]
Each component means:
Link definition
The library is intended to help you to build a single source-of-truth for action definition and data.
It defines a sealed hierarchy
of deep-link actions used across all of your servers and client applications.
You could use path, search and hash parts of URI to build an Action - what needs to be done in a client
application when deep-link is processed.
To distinguish between applications an (or) domains use scheme and host components of URI. Check-out some sample
LinkBuilder/LinkParser`
to learn how to define your deep-link scheme.
Building your links
Hint: Refer to testaction module for an example.
Dependencies and project setup
Create a new Kotlin-multiplatform project and add the library dependency to your build file:
sourceSets {
val commonMain by getting {
dependencies {
implementation("com.motorro.keeplink:deeplink:x.x.x")
implementation(libs.kotlin.serialization.core)
}
}
}
Define your required build targets and outputs (see the sample for reference):
iOS framework:
val iosArm64 = iosArm64("iosArm64")
val iosX64 = iosX64("iosX64")
configure(listOf(iosArm64, iosX64)) {
binaries {
framework(listOf(RELEASE))
}
}
val outputIos by tasks.creating(org.jetbrains.kotlin.gradle.tasks.FatFrameworkTask::class) {
group = "output"
destinationDir = file("$projectDir/output/ios/Fat")
from(
iosArm64.binaries.getFramework("RELEASE"),
iosX64.binaries.getFramework("RELEASE")
)
}
NPM module for Node.js or a browser:
js(IR) {
moduleName = "yourmodulename"
compilations.all {
kotlinOptions.freeCompilerArgs += listOf(
"-opt-in=kotlin.js.ExperimentalJsExport"
)
}
generateTypeScriptDefinitions()
binaries.library()
useCommonJs()
nodejs {
testTask {
useMocha {
timeout = "10s"
}
}
@OptIn(ExperimentalDistributionDsl::class)
distribution {
outputDirectory.set(file("$projectDir/output/npm"))
}
}
}
Build documentation
Add a Dokka task to your project to build the documentation. Then you could ship int
to your fellow developers and marketing team to keep a link registry actual:
subprojects {
tasks {
withType<DokkaTask>().configureEach {
dokkaSourceSets.configureEach {
includes.from("moduledoc.md")
}
}
withType<DokkaTaskPartial>().configureEach {
dokkaSourceSets.configureEach {
includes.from("moduledoc.md")
}
}
}
}
Build your link structure
To build your link structure inherit the Action:
You define the structure in any way that best works for you best. The action implements the PSHComponents
which defines the path, search, and hash components for your resulting URI when building.
Refer to TestAction definition to know more
Create parsers
To use some handy library tools, create your parsers by implementing an ActionParser:
fun interface ActionParser<out A : Action> {
fun parse(components: PshComponents, pathIndex: Int): A?
}
The parser accepts two parameters:
components - parsed URI path, search, and hash components.
pathIndex - current path segment index being processed. Optionally used by utility parsers (see below)
The parser returns a parsed action or a null if parse fails or irrelevant.
For example:
internal val MagicLinkHashParser = ActionParser { components, pathIndex ->
components.getPath().getOrNull(pathIndex)?.takeIf { it.isNotBlank() }?.let { TestAction.Login.Magic(it) }
}
Bind your schemes
To be completely type-safe create some predefined scheme/host builders and parsers that will build the URI string for you
given a deep-link object. To do so use LinkBuilder
and LinkParser utilities:
@JsExport
@OptIn(ExperimentalJsExport::class)
object LinkParsers {
val MOTORRO: LinkParser<TestAction> = SchemeHostLinkParser(RootActionParser, "motorro", "")
}
LinkBuilders {
MOTORRO: LinkBuilder<TestAction> = SchemeHostLinkBuilder(, )
}
That's basically it. Now build your project and assemble the output artifacts.
Consuming deep-links
Add your artifacts to the target project. Use a pre-defined parser to parse your deep-link:
val parser = LinkParsers.MOTORRO
val link = parser.parse("motorro:/login/magic/123")
val utm = link?.utm
if (null != utm) {
}
when(link.action) {
is TestLink.Login.Magic -> {
}
else -> {
}
}
Create deep-links
Imagine you have a Node backend that provides data to your UI. Let's create a link for them:
const builder = LinkBuilders.MOTORRO;
const action = new TestAction.Login.Magic();
link = (action).(());
linkStr = builder.(link);
Check-out the complete Node example and tests
to learn more.
Some handy parsers included
The library comes with some handy parsers if you like. Although you are not required to use them, they could potentially
speed up your parsing and build parsers in more or less declarative way. See the complete parser setup
in the testaction project.
SegmentCheckParser
Checks that current segment being parsed matches your string and calls the next parser.
Used to traverse your path:
internal val MagicLinkParser = SegmentCheckParser(
TestAction.Login.Magic.SEGMENT,
BranchActionParser(listOf(MagicLinkHashParser)) { _, _ -> TestAction.Invalid.INSTANCE }
)
The parser checks for correct segment (that is /login/magic/) and passes control to the token parser.
DefaultActionParser
This one is simple. It just returns the action you produce in action parameter. May be used to return the action to
some intermediate segment if all it's children do not match (see BranchActionParser below):
internal val ProfileParser = SegmentCheckParser(
TestAction.Profile.SEGMENT,
DefaultActionParser { TestAction.Profile() }
)
BranchActionParser
Iterates its children to find the first that returns a non-null result. If none answers positive - runs the default
fallback:
private val rootParsers = listOf(
ProfileParser,
LoginParser,
SearchParser
)
val RootActionParser = BranchActionParser(rootParsers) { components, _ ->
if (components.getPath().isEmpty()) TestAction.Root() else TestAction.Unknown(components)
}
Conclusion
I hope someone finds the given approach (and the library) to deep-link management handy. As for me, it gives a more or
less complete solution to both the world of developers and the management. The proposed way aims to be a single source
of truth for deep-links in your project providing a write-once solution for both the coding and the documenting tasks.
The approach keeps you from implementation errors and also saves your time when building and processing the links.