Skip to content

Commit

Permalink
Merge pull request #2 from sergejsha/backpress-support
Browse files Browse the repository at this point in the history
BackNavigation + Tests
  • Loading branch information
sergejsha authored Jul 22, 2024
2 parents e458dde + 0d4ee30 commit f8ef590
Show file tree
Hide file tree
Showing 20 changed files with 608 additions and 22 deletions.
49 changes: 49 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Check

permissions:
checks: write

on:
pull_request:
paths-ignore:
- 'documentation/**'
- '*.md'
push:
branches:
- master
paths-ignore:
- 'documentation/**'
- '*.md'

jobs:
test:
name: Run tests

strategy:
matrix:
include:
- os: ubuntu-latest
task: testDebugUnit # jvm + android
- os: macos-latest
task: iosSimulatorArm64

runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup JDK 17
uses: actions/setup-java@v4
with:
distribution: 'adopt'
java-version: '17'

- name: Run ${{ matrix.task }} test
run: ./gradlew ${{ matrix.task }}Test --stacktrace

- name: Publish ${{ matrix.task }} report
uses: mikepenz/action-junit-report@v4
if: success() || failure()
with:
check_name: ${{ matrix.task }}
report_paths: "componental/build/test-results/${{ matrix.task }}Test/TEST-*.xml"
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,14 @@ dependencies {

# Publishing

1. Bump version in `root.publication.gradle.kts` of the root project
1. Bump version in `root.publication.gradle.kts`
2. `./gradlew clean build publishAllPublicationsToCentralRepository`

## Local maven

1. Set `X.X-SNAPSHOT` version in `root.publication.gradle.kts`
2. `./gradlew clean build publishToMavenLocal`

# Release Notes

* 0.2 Module `componental` is exposed as API from `componental.compose`
Expand Down
11 changes: 11 additions & 0 deletions componental-compose/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,14 @@ android {
minSdk = libs.versions.android.sdk.min.get().toInt()
}
}

// https://youtrack.jetbrains.com/issue/KT-61313
tasks.withType<Sign>().configureEach {
val publicationName = name.removePrefix("sign").removeSuffix("Publication")
tasks.findByName("linkDebugTest$publicationName")?.let {
mustRunAfter(it)
}
tasks.findByName("compileTestKotlin$publicationName")?.let {
mustRunAfter(it)
}
}
19 changes: 18 additions & 1 deletion componental/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ plugins {
kotlin {
explicitApi()

jvm()
jvm {
@OptIn(ExperimentalKotlinGradlePluginApi::class)
compilerOptions {
jvmTarget.set(JvmTarget.JVM_1_8)
}
}
androidTarget {
publishLibraryVariants("release")
@OptIn(ExperimentalKotlinGradlePluginApi::class)
Expand All @@ -36,6 +41,7 @@ kotlin {
}
commonTest.dependencies {
implementation(libs.kotlin.test)
implementation(libs.kotlinx.coroutines.test)
}
}
}
Expand All @@ -47,3 +53,14 @@ android {
minSdk = libs.versions.android.sdk.min.get().toInt()
}
}

// https://youtrack.jetbrains.com/issue/KT-61313
tasks.withType<Sign>().configureEach {
val publicationName = name.removePrefix("sign").removeSuffix("Publication")
tasks.findByName("linkDebugTest$publicationName")?.let {
mustRunAfter(it)
}
tasks.findByName("compileTestKotlin$publicationName")?.let {
mustRunAfter(it)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/** Copyright 2024 Halfbit GmbH, Sergej Shafarenka */
package de.halfbit.componental

import de.halfbit.componental.back.BackNavigationOwner
import de.halfbit.componental.coroutines.CoroutineScopeOwner
import de.halfbit.componental.lifecycle.Lifecycle
import de.halfbit.componental.lifecycle.LifecycleOwner
Expand All @@ -11,7 +13,7 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch

public interface ComponentContext : LifecycleOwner, CoroutineScopeOwner, RestoratorOwner {
public interface ComponentContext : LifecycleOwner, CoroutineScopeOwner, RestoratorOwner, BackNavigationOwner {

public companion object {
public fun create(
Expand Down Expand Up @@ -47,9 +49,12 @@ private class DefaultComponentContext(
override val lifecycle: Lifecycle,
override val coroutineScope: CoroutineScope,
override val restorator: Restorator,
) : ComponentContext
) : ComponentContext,
BackNavigationOwner by BackNavigationOwner.create(lifecycle)

internal inline fun ComponentContext.doOnDestroy(crossinline callback: () -> Unit): ComponentContext {
internal inline fun ComponentContext.doOnDestroy(
crossinline callback: () -> Unit
): ComponentContext {
lifecycle.subscribe(
object : Lifecycle.Subscriber.Callbacks {
override fun onDestroy() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/** Copyright 2024 Halfbit GmbH, Sergej Shafarenka */
package de.halfbit.componental.back

public interface BackNavigation {
public fun register(onNavigateBack: OnNavigateBack)

public companion object {
private val callbacks: MutableList<OnNavigateBack> = mutableListOf()

internal fun register(callback: OnNavigateBack) {
callbacks += callback
}

internal fun unregister(callback: OnNavigateBack) {
callbacks -= callback
}

/** To be called by the screen holding compose UI, e.g. by an activity on android. */
public fun dispatchOnNavigateBack() {
callbacks.toList().lastOrNull()?.onNavigateBack()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/** Copyright 2024 Halfbit GmbH, Sergej Shafarenka */
package de.halfbit.componental.back

import de.halfbit.componental.lifecycle.Lifecycle

public interface BackNavigationOwner {
public val backNavigation : BackNavigation

public companion object {
public fun create(lifecycle: Lifecycle): BackNavigationOwner =
object : BackNavigationOwner {
override val backNavigation: BackNavigation =
object : BackNavigation {
override fun register(onNavigateBack: OnNavigateBack) {
lifecycle.subscribe(
object : Lifecycle.Subscriber.Callbacks {
override fun onResume() {
BackNavigation.register(onNavigateBack)
}

override fun onPause() {
BackNavigation.unregister(onNavigateBack)
}
}
)
}
}
}
}
}

public inline fun BackNavigationOwner.onNavigateBack(onNavigateBack: OnNavigateBack) {
backNavigation.register(onNavigateBack)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/** Copyright 2024 Halfbit GmbH, Sergej Shafarenka */
package de.halfbit.componental.back

public fun interface OnNavigateBack {
public fun onNavigateBack()
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/** Copyright 2024 Halfbit GmbH, Sergej Shafarenka */
package de.halfbit.componental.router

import kotlinx.coroutines.flow.Flow
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/** Copyright 2024 Halfbit GmbH, Sergej Shafarenka */
package de.halfbit.componental.router.slot

import de.halfbit.componental.ComponentContext
Expand Down Expand Up @@ -58,7 +59,7 @@ public fun <Id : Any, Child : Any> ComponentContext.childSlot(
val context = createChildContext(
childLifecycle = childLifecycle,
childCoroutineScope = coroutineScope.createChildCoroutineScope(tag),
restorator = Restorator(restoredChildState)
restorator = Restorator(restoredChildState),
)
val node = RouteNode(id, childFactory(id, context))
return RuntimeRouteNode(
Expand All @@ -71,7 +72,7 @@ public fun <Id : Any, Child : Any> ComponentContext.childSlot(
}
}

fun Id?.asSlot(): Slot<Id, Child> {
fun Id?.asRuntimeSlot(): Slot<Id, Child> {
val oldNode = runtimeNode
val newNode = when (this) {
null -> {
Expand Down Expand Up @@ -108,14 +109,14 @@ public fun <Id : Any, Child : Any> ComponentContext.childSlot(
}

return flow {
emit(activeId.asSlot())
emit(activeId.asRuntimeSlot())
router.events.collect { event ->
activeId = event.transform(activeId)
emit(activeId.asSlot())
emit(activeId.asRuntimeSlot())
}
}.stateIn(
coroutineScope,
SharingStarted.Eagerly,
initial.asSlot(),
initial.asRuntimeSlot(),
)
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/** Copyright 2024 Halfbit GmbH, Sergej Shafarenka */
package de.halfbit.componental.router.stack

import de.halfbit.componental.ComponentContext
Expand Down Expand Up @@ -32,7 +33,7 @@ public class StackRouter<I : Any> : Router<Event<I>>() {

public fun <I : Any> StackRouter<I>.push(id: I) {
route(
event = Event { keys -> keys + id }
event = Event { ids -> ids + id }
)
}

Expand All @@ -56,7 +57,6 @@ public fun <Id : Any, Child : Any> ComponentContext.childStack(
childFactory: (id: Id, context: ComponentContext) -> Child,
): StateFlow<Stack<Id, Child>> {
val runtimeNodes = mutableMapOf<Id, RuntimeRouteNode<Id, Child>>()

val restoredRoute: Map<Id, RestorableRoute<Id>>? =
restorator.restoreRoute()?.let {
ProtoBuf.decodeFromByteArray(
Expand All @@ -71,7 +71,7 @@ public fun <Id : Any, Child : Any> ComponentContext.childStack(
val context = createChildContext(
childLifecycle = childLifecycle,
childCoroutineScope = coroutineScope.createChildCoroutineScope(tag),
restorator = Restorator(restoredChildState)
restorator = Restorator(restoredChildState),
)
val node = RouteNode(id, childFactory(id, context))
return RuntimeRouteNode(
Expand All @@ -84,10 +84,10 @@ public fun <Id : Any, Child : Any> ComponentContext.childStack(
}
}

fun Collection<Id>.asStack(): Stack<Id, Child> {
fun Collection<Id>.asRuntimeStack(): Stack<Id, Child> {

val stackNodes = map { key ->
runtimeNodes[key] ?: createRuntimeRouteNode(key)
val stackNodes = map { id ->
runtimeNodes[id] ?: createRuntimeRouteNode(id)
}

stackNodes.reversed().forEachIndexed { index, node ->
Expand Down Expand Up @@ -118,22 +118,22 @@ public fun <Id : Any, Child : Any> ComponentContext.childStack(
}

return flow {
emit(ids.asStack())
emit(ids.asRuntimeStack())
router.events.collect { event ->
ids = event.transform(ids)
emit(ids.asStack())
emit(ids.asRuntimeStack())
}
}.stateIn(
coroutineScope,
SharingStarted.Eagerly,
initial.asStack(),
initial.asRuntimeStack(),
)
}

private fun <I : Any, C : Any> List<RouteNode<I, C>>.toStack(): Stack<I, C> {
check(isNotEmpty()) { "List used as a stack have at least one entry" }
check(isNotEmpty()) { "List used as a stack must have at least one entry" }
return Stack(
active = last(),
inactive = if (size == 1) emptyList() else subList(1, lastIndex),
inactive = if (size == 1) emptyList() else subList(0, lastIndex),
)
}
Loading

0 comments on commit f8ef590

Please sign in to comment.