Skip to content

Commit

Permalink
Initial
Browse files Browse the repository at this point in the history
  • Loading branch information
Reedyuk committed Nov 25, 2022
1 parent a81db06 commit ab1ce60
Show file tree
Hide file tree
Showing 19 changed files with 505 additions and 34 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- name: Build
run: ./gradlew build
- name: Run create xcframework
run: ./gradlew assembleLibraryXCFramework
run: ./gradlew assembleDirektXCFramework
- name: Run publish
run: ./gradlew publish
env:
Expand All @@ -51,7 +51,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ github.token }}
with:
upload_url: https://uploads.github.com/repos/MyUNiDAYS/libraryname/releases/${{ steps.lastRelease.outputs.id }}/assets?name=${{ github.ref_name }}.zip
upload_url: https://uploads.github.com/repos/MyUNiDAYS/Direkt/releases/${{ steps.lastRelease.outputs.id }}/assets?name=${{ github.ref_name }}.zip
asset_path: build/${{ github.ref_name }}.zip
asset_name: ${{ github.ref_name }}.zip
asset_content_type: application/zip
8 changes: 8 additions & 0 deletions .idea/artifacts/direkt_jslegacy_0_0_7.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

61 changes: 40 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,44 +1,63 @@
<h1 align="left">Template Kotlin Library
<img alt="GitHub last commit" src="https://img.shields.io/github/last-commit/MyUNiDAYS/template-kotlin-library?style=flat-square"> <a href="https://git.live"><img src="https://img.shields.io/badge/collaborate-on%20gitlive-blueviolet?style=flat-square"></a>
</h1>
<h1 align="left">DireKT Kotlin SDK <img alt="GitHub last commit" src="https://img.shields.io/github/last-commit/myunidays/direkt?style=flat-square"> <a href="https://git.live"><img src="https://img.shields.io/badge/collaborate-on%20gitlive-blueviolet?style=flat-square"></a></h1>

The Template Kotlin Library SDK
The DireKT Kotlin SDK is a completely decoupled Routing library that is not platform specific.

## Installation

### KMM

```
implementation("com.myunidays:package:0.0.1")
implementation("com.myunidays:direkt:0.0.5")
```

### Android
## How to use

```
implementation("com.myunidays:package-android:0.0.1")
We recommend using the coordinator pattern but it's not neccessary.

Create a subclass of the RoutingConfig, expose the screens you want to route and what constructor params they need.
```kotlin
sealed class RootConfig(key: String): RoutingConfig(key) {
object Dashboard: RootConfig("Dashboard")
object Standard: RootConfig("Standard")
}
```

### iOS
Create a function to create those screens.
```kotlin
fun createChild(config: RootConfig): ScreenInterface = when (config) {
RootConfig.Dashboard -> DashboardViewModel()
RootConfig.Standard -> StandardViewModel()
}
```

Add to the binary to your swift package like this:
Then create an instance of the RouterImpl or implement the interface Router.
```kotlin
val router = RouterImpl<RootConfig, ScreenInterface>(
RootConfig.Dashboard,
::configForName
)
```

```swift
.binaryTarget(
name: "project",
url: "https://github.com/MyUNiDAYS/template-kotlin-library/releases/download/0.0.1/0.0.1.zip",
checksum: "8c35293a410f4ec5d150c4f5464f6b5cf04a1a15d1ae9c29126bb0b7a7dc2a54"
),
To listen to route changes
```kotlin
router.stack.collect { (transition, config) ->
if (transition == Transition.Push) {
println("Pushed route $config")
}
```

Where 0.0.1 is the release number, you will also need to change the checksum, xcode will tell you the different checksum if its wrong and just update that from the error message.
To request a route change, where RootConfig.Standard is an entry in the config defined before.
```kotlin
router.push(RootConfig.Standard)
```

## How to use
## Examples

### KMM
In the Examples folder, there is an example using KMM with coordinators targeting iOS and Android.

### Android
## Known Issues

### iOS
Currently, basic support for Deeplinking.

## Contributing

Expand Down
20 changes: 18 additions & 2 deletions library/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ val PUBLISH_NAME: String by project
group = MODULE_PACKAGE_NAME
version = MODULE_VERSION_NUMBER

val coroutines_version = "1.6.0-native-mt"
val ktor_version = "2.0.0"

plugins {
kotlin("multiplatform")
id("com.android.library")
Expand Down Expand Up @@ -42,7 +45,13 @@ tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {

kotlin {
js(BOTH) {
browser { }
browser {
testTask {
useKarma {
useChromeHeadless()
}
}
}
}
android {
publishAllLibraryVariants()
Expand All @@ -62,10 +71,17 @@ kotlin {
}
}
sourceSets {
val commonMain by getting
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version")
implementation("io.ktor:ktor-http:$ktor_version")
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version")
implementation("app.cash.turbine:turbine:0.7.0")
}
}
val jsMain by getting
Expand Down
16 changes: 8 additions & 8 deletions library/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ signing.keyId=""
signing.password=""

MODULE_PACKAGE_NAME=com.myunidays
MODULE_VERSION_NUMBER=0.0.1
MODULE_NAME=template-kotlin-library
MODULE_VERSION_NUMBER=0.0.7
MODULE_NAME=Direkt

OPEN_SOURCE_REPO=https://oss.sonatype.org/service/local/staging/deploy/maven2/
PUBLISH_NAME=template-kotlin-library
PUBLISH_DESCRIPTION=
PUBLISH_URL=https://github.com/MyUNiDAYS/template-kotlin-library
PUBLISH_NAME=direkt
PUBLISH_DESCRIPTION=KMM Routing
PUBLISH_URL=https://github.com/MyUNiDAYS/DireKt

PUBLISH_SCM_URL=https://github.com/MyUNiDAYS/template-kotlin-library
PUBLISH_SCM_CONNECTION=scm:git://git@github.com:MyUNiDAYS/template-kotlin-library.git
PUBLISH_SCM_DEVELOPERCONNECTION=scm:git:ssh://git@github.com:MyUNiDAYS/template-kotlin-library.git
PUBLISH_SCM_URL=https://github.com/MyUNiDAYS/DireKt
PUBLISH_SCM_CONNECTION=scm:git://git@github.com:MyUNiDAYS/DireKt.git
PUBLISH_SCM_DEVELOPERCONNECTION=scm:git:ssh://git@github.com:MyUNiDAYS/DireKt.git
2 changes: 1 addition & 1 deletion library/src/androidMain/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.myunidays.library"/>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.myunidays.direkt"/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.myunidays.dispatcher

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.test.TestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.setMain

@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
actual class CoroutineTestDispatchersManager actual constructor(actual val testDispatcher: TestDispatcher) {

// We can use a different dispatched when running tests for Android
actual val dispatcherProvider = object : DispatcherProvider {
override fun default(): CoroutineDispatcher = testDispatcher
override fun main(): CoroutineDispatcher = testDispatcher
override fun unconfined(): CoroutineDispatcher = testDispatcher
}

actual fun start() {
Dispatchers.setMain(testDispatcher)
}

actual fun stop() {
Dispatchers.resetMain()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.myunidays.dispatcher

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers

interface DispatcherProvider {

fun main(): CoroutineDispatcher
fun default(): CoroutineDispatcher
fun unconfined(): CoroutineDispatcher
}

class DefaultDispatcherProvider : DispatcherProvider {
override fun main(): CoroutineDispatcher = Dispatchers.Main
override fun default(): CoroutineDispatcher = Dispatchers.Default
override fun unconfined(): CoroutineDispatcher = Dispatchers.Unconfined
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.myunidays.router

import com.myunidays.transition.Transition
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

// We can use this empty router for the 'native' views which are unbound by KMM - maybe could be used for testing too.
class EmptyRouterImpl<Config : RoutingConfig, Child> : Router<Config, Child> {
override val stack = MutableSharedFlow<Pair<Transition, Config>>(0)
override val stackHistory: StateFlow<List<Pair<Transition, Config>>> = MutableStateFlow(emptyList())
override val canGoBack = false

override val activeChild: Config? = null

override suspend fun handleDeeplink(deeplink: String) = throw EmptyRouterException()

class EmptyRouterException : Exception("Cannot call method on Empty Router")
}
18 changes: 18 additions & 0 deletions library/src/commonMain/kotlin/com/myunidays/router/Router.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.myunidays.router

import com.myunidays.transition.Transition
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow

// The router is COMPLETELY decoupled from the project, its using Generics
// and doesnt need to know about view models etc.
// Consider how we could potentially handle deeplinks from here?? -
// using the key from Routing config.
interface Router<Config : RoutingConfig, Child> {
val stack: SharedFlow<Pair<Transition, Config>>
val stackHistory: StateFlow<List<Pair<Transition, Config>>>
val activeChild: Config?
val canGoBack: Boolean
// deeplink stuff
suspend fun handleDeeplink(deeplink: String): String?
}
88 changes: 88 additions & 0 deletions library/src/commonMain/kotlin/com/myunidays/router/RouterImpl.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.myunidays.router

import com.myunidays.dispatcher.DefaultDispatcherProvider
import com.myunidays.dispatcher.DispatcherProvider
import com.myunidays.transition.Transition
import io.ktor.http.Url
import io.ktor.util.toMap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

class RouterImpl<Config : RoutingConfig, Child>(
initial: Config,
private val configForName: (name: String, params: Map<String, List<String>>) -> Config?,
dispatcher: DispatcherProvider = DefaultDispatcherProvider()
) : Router<Config, Child>, RouterTransitions<Config, Child> {

override val activeChild: Config? get() = _stack.lastOrNull()
override val stack: MutableSharedFlow<Pair<Transition, Config>> = MutableSharedFlow(1)
private val _stack: MutableList<Config> = mutableListOf()

// Jacobs Idea: We could store the whole list of changes, so that we could replay in the case of a crash.
private val _stackHistory: MutableStateFlow<List<Pair<Transition, Config>>> = MutableStateFlow(emptyList())
override val stackHistory: StateFlow<List<Pair<Transition, Config>>> = _stackHistory

override val canGoBack: Boolean get() = _stack.size > 1

init {
CoroutineScope(dispatcher.default()).launch {
stack.collect { transitionConfig ->
_stackHistory.emit(_stackHistory.value + transitionConfig)
}
}
CoroutineScope(dispatcher.default()).launch {
push(initial)
}
}

override suspend fun push(config: Config) {
_stack.add(config)
stack.emit(Transition.Push to config)
}

override suspend fun pop() {
_stack.removeLast()
stack.emit(Transition.Pop to _stack.last())
}

override suspend fun replace(config: Config) {
_stack.removeLast()
_stack.add(config)
stack.emit(Transition.Replace to config)
}

override suspend fun update(config: Config) {
_stack.add(config)
stack.emit(Transition.Update to config)
}

// we could have a more generic function, router.supportsConfig(key)?
// Deeplinking stuff
override suspend fun handleDeeplink(deeplink: String): String? =
runCatching {
Url(deeplink).let { deeplinkUrl ->
val parameters = deeplinkUrl.parameters.toMap()
if (parameters[Transition.key]?.firstOrNull() == Transition.Pop.name) {
pop()
return deeplink
}
return@runCatching configForName(
deeplinkUrl.host,
parameters
)?.let { config ->
when (config.transition) {
Transition.Push -> push(config)
Transition.Pop -> pop()
Transition.Replace -> replace(config)
Transition.Update -> update(config)
}
return deeplink
}
}
}
.onFailure { println("Failed to parse deeplink $deeplink $it") }
.getOrNull()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.myunidays.router

internal interface RouterTransitions<Config : RoutingConfig, Child> {
suspend fun push(config: Config)
suspend fun pop()
suspend fun replace(config: Config)
suspend fun update(config: Config)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.myunidays.router

import com.myunidays.transition.Transition

open class RoutingConfig(
val key: String,
val transition: Transition = Transition.Push
)
Loading

0 comments on commit ab1ce60

Please sign in to comment.