Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CircuitX Navigation #1669

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions circuitx/navigation/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright (C) 2022 Slack Technologies, LLC
// SPDX-License-Identifier: Apache-2.0
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl

plugins {
alias(libs.plugins.agp.library)
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.compose)
alias(libs.plugins.mavenPublish)
}

kotlin {
// region KMP Targets
androidTarget { publishLibraryVariants("release") }
jvm()
iosX64()
iosArm64()
iosSimulatorArm64()
macosX64()
macosArm64()
js(IR) {
moduleName = property("POM_ARTIFACT_ID").toString()
browser()
}
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
moduleName = property("POM_ARTIFACT_ID").toString()
browser()
}
// endregion

applyDefaultHierarchyTemplate()

sourceSets {
commonMain {
dependencies {
api(libs.compose.runtime)
api(libs.coroutines)
api(projects.circuitFoundation)
}
}

androidMain { dependencies { api(projects.circuitx.android) } }

// We use a common folder instead of a common source set because there is no commonizer
// which exposes the browser APIs across these two targets.
jsMain { kotlin.srcDir("src/browserMain/kotlin") }
wasmJsMain { kotlin.srcDir("src/browserMain/kotlin") }
}
}

android { namespace = "com.slack.circuitx.navigation" }
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
androidx.activity:activity-compose
androidx.activity:activity-ktx
androidx.activity:activity
androidx.annotation:annotation-experimental
androidx.annotation:annotation-jvm
androidx.annotation:annotation
androidx.arch.core:core-common
androidx.arch.core:core-runtime
androidx.autofill:autofill
androidx.collection:collection-jvm
androidx.collection:collection-ktx
androidx.collection:collection
androidx.compose.animation:animation-android
androidx.compose.animation:animation-core-android
androidx.compose.animation:animation-core
androidx.compose.animation:animation
androidx.compose.foundation:foundation-android
androidx.compose.foundation:foundation-layout-android
androidx.compose.foundation:foundation-layout
androidx.compose.foundation:foundation
androidx.compose.runtime:runtime-android
androidx.compose.runtime:runtime-saveable-android
androidx.compose.runtime:runtime-saveable
androidx.compose.runtime:runtime
androidx.compose.ui:ui-android
androidx.compose.ui:ui-geometry-android
androidx.compose.ui:ui-geometry
androidx.compose.ui:ui-graphics-android
androidx.compose.ui:ui-graphics
androidx.compose.ui:ui-text-android
androidx.compose.ui:ui-text
androidx.compose.ui:ui-unit-android
androidx.compose.ui:ui-unit
androidx.compose.ui:ui-util-android
androidx.compose.ui:ui-util
androidx.compose.ui:ui
androidx.concurrent:concurrent-futures
androidx.core:core-ktx
androidx.core:core
androidx.customview:customview-poolingcontainer
androidx.emoji2:emoji2
androidx.interpolator:interpolator
androidx.lifecycle:lifecycle-common-jvm
androidx.lifecycle:lifecycle-common
androidx.lifecycle:lifecycle-livedata-core
androidx.lifecycle:lifecycle-process
androidx.lifecycle:lifecycle-runtime-android
androidx.lifecycle:lifecycle-runtime-ktx-android
androidx.lifecycle:lifecycle-runtime-ktx
androidx.lifecycle:lifecycle-runtime
androidx.lifecycle:lifecycle-viewmodel-android
androidx.lifecycle:lifecycle-viewmodel-compose-android
androidx.lifecycle:lifecycle-viewmodel-compose
androidx.lifecycle:lifecycle-viewmodel-ktx
androidx.lifecycle:lifecycle-viewmodel-savedstate
androidx.lifecycle:lifecycle-viewmodel
androidx.profileinstaller:profileinstaller
androidx.savedstate:savedstate-ktx
androidx.savedstate:savedstate
androidx.startup:startup-runtime
androidx.tracing:tracing
androidx.versionedparcelable:versionedparcelable
com.google.guava:listenablefuture
org.jetbrains.compose.foundation:foundation
org.jetbrains.compose.runtime:runtime-saveable
org.jetbrains.compose.runtime:runtime
org.jetbrains.compose.ui:ui
org.jetbrains.kotlin:kotlin-android-extensions-runtime
org.jetbrains.kotlin:kotlin-bom
org.jetbrains.kotlin:kotlin-parcelize-runtime
org.jetbrains.kotlin:kotlin-stdlib
org.jetbrains.kotlinx:atomicfu-jvm
org.jetbrains.kotlinx:atomicfu
org.jetbrains.kotlinx:kotlinx-collections-immutable-jvm
org.jetbrains.kotlinx:kotlinx-collections-immutable
org.jetbrains.kotlinx:kotlinx-coroutines-android
org.jetbrains.kotlinx:kotlinx-coroutines-bom
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm
org.jetbrains.kotlinx:kotlinx-coroutines-core
org.jetbrains:annotations
56 changes: 56 additions & 0 deletions circuitx/navigation/dependencies/jvmRuntimeClasspath.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
androidx.annotation:annotation-jvm
androidx.annotation:annotation
androidx.arch.core:core-common
androidx.collection:collection-jvm
androidx.collection:collection
androidx.lifecycle:lifecycle-common-jvm
androidx.lifecycle:lifecycle-common
androidx.lifecycle:lifecycle-runtime-desktop
androidx.lifecycle:lifecycle-runtime
androidx.lifecycle:lifecycle-viewmodel-desktop
androidx.lifecycle:lifecycle-viewmodel
org.jetbrains.androidx.lifecycle:lifecycle-common
org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose-desktop
org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose
org.jetbrains.androidx.lifecycle:lifecycle-runtime
org.jetbrains.androidx.lifecycle:lifecycle-viewmodel
org.jetbrains.compose.animation:animation-core-desktop
org.jetbrains.compose.animation:animation-core
org.jetbrains.compose.animation:animation-desktop
org.jetbrains.compose.animation:animation
org.jetbrains.compose.annotation-internal:annotation
org.jetbrains.compose.collection-internal:collection
org.jetbrains.compose.foundation:foundation-desktop
org.jetbrains.compose.foundation:foundation-layout-desktop
org.jetbrains.compose.foundation:foundation-layout
org.jetbrains.compose.foundation:foundation
org.jetbrains.compose.runtime:runtime-desktop
org.jetbrains.compose.runtime:runtime-saveable-desktop
org.jetbrains.compose.runtime:runtime-saveable
org.jetbrains.compose.runtime:runtime
org.jetbrains.compose.ui:ui-desktop
org.jetbrains.compose.ui:ui-geometry-desktop
org.jetbrains.compose.ui:ui-geometry
org.jetbrains.compose.ui:ui-graphics-desktop
org.jetbrains.compose.ui:ui-graphics
org.jetbrains.compose.ui:ui-text-desktop
org.jetbrains.compose.ui:ui-text
org.jetbrains.compose.ui:ui-unit-desktop
org.jetbrains.compose.ui:ui-unit
org.jetbrains.compose.ui:ui-util-desktop
org.jetbrains.compose.ui:ui-util
org.jetbrains.compose.ui:ui
org.jetbrains.kotlin:kotlin-bom
org.jetbrains.kotlin:kotlin-stdlib-jdk7
org.jetbrains.kotlin:kotlin-stdlib-jdk8
org.jetbrains.kotlin:kotlin-stdlib
org.jetbrains.kotlinx:atomicfu-jvm
org.jetbrains.kotlinx:atomicfu
org.jetbrains.kotlinx:kotlinx-collections-immutable-jvm
org.jetbrains.kotlinx:kotlinx-collections-immutable
org.jetbrains.kotlinx:kotlinx-coroutines-bom
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm
org.jetbrains.kotlinx:kotlinx-coroutines-core
org.jetbrains.skiko:skiko-awt
org.jetbrains.skiko:skiko
org.jetbrains:annotations
3 changes: 3 additions & 0 deletions circuitx/navigation/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
POM_ARTIFACT_ID=circuitx-navigation
POM_NAME=CircuitX (Navigation)
POM_DESCRIPTION=CircuitX (Navigation)
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (C) 2024 Slack Technologies, LLC
// SPDX-License-Identifier: Apache-2.0
package com.slack.circuitx.navigation.intercepting

import android.content.Context
import com.slack.circuit.runtime.screen.Screen
import com.slack.circuitx.android.AndroidScreen
import com.slack.circuitx.android.AndroidScreenStarter
import com.slack.circuitx.android.IntentScreen
import com.slack.circuitx.android.rememberAndroidScreenAwareNavigator

/** A [CircuitNavigationInterceptor] version of [rememberAndroidScreenAwareNavigator] */
public class AndroidScreenAwareNavigationInterceptor(private val starter: AndroidScreenStarter) :
CircuitNavigationInterceptor {

public constructor(
context: Context
) : this(
AndroidScreenStarter { screen ->
when (screen) {
is IntentScreen -> screen.startWith(context)
else -> false
}
}
)

override fun goTo(screen: Screen): CircuitNavigationInterceptor.Result {
return when (screen) {
is AndroidScreen ->
if (starter.start(screen)) {
CircuitNavigationInterceptor.ConsumedSuccess
} else {
CircuitNavigationInterceptor.Result.Failure(consumed = true)
}
else -> CircuitNavigationInterceptor.Skipped
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (C) 2024 Slack Technologies, LLC
// SPDX-License-Identifier: Apache-2.0
package com.slack.circuitx.navigation.intercepting

import androidx.compose.runtime.Composable
import com.slack.circuit.backstack.SaveableBackStack
import com.slack.circuit.foundation.Navigator
import com.slack.circuit.foundation.rememberCircuitNavigator
import com.slack.circuit.runtime.Navigator
import com.slack.circuit.runtime.screen.PopResult

/**
* Provides a [Navigator] that is delegated to by the [CircuitInterceptingNavigator] if navigation
* was not intercepted by a [CircuitNavigationInterceptor].
*
* @param onRootPop A handler for the root pop. Null indicates that the root pop should be called by
* the system back handler.
*/
@Composable
public actual fun rememberCircuitInterceptingBackStackNavigator(
backStack: SaveableBackStack,
onRootPop: ((result: PopResult?) -> Unit)?,
): Navigator {
return if (onRootPop != null) {
rememberCircuitNavigator(backStack = backStack, onRootPop = onRootPop)
} else {
rememberCircuitNavigator(backStack = backStack, enableBackHandler = true)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (C) 2024 Slack Technologies, LLC
// SPDX-License-Identifier: Apache-2.0
package com.slack.circuitx.navigation.intercepting

import android.util.Log
import com.slack.circuit.runtime.screen.PopResult
import com.slack.circuit.runtime.screen.Screen
import kotlinx.collections.immutable.ImmutableList

/** A [CircuitNavigationEventListener] that adds Logcat logging for Circuit navigation. */
public object LoggingNavigationEventListener : CircuitNavigationEventListener {

override fun onBackStackChanged(backStack: ImmutableList<Screen>) {
log("new backstack ${backStack.joinToString { it.loggingName() ?: "" }}")
}

override fun goTo(screen: Screen) {
log("goTo ${screen.loggingName()}")
}

override fun pop(backStack: ImmutableList<Screen>, result: PopResult?) {
log("pop ${backStack.firstOrNull()?.loggingName()}")
}

private fun log(message: String) = Log.i("Circuit Navigation", message)
}

private fun Screen.loggingName() = this::class.simpleName
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (C) 2024 Slack Technologies, LLC
// SPDX-License-Identifier: Apache-2.0
package com.slack.circuitx.navigation.intercepting

import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.slack.circuit.backstack.NavDecoration
import com.slack.circuit.backstack.rememberSaveableBackStack
import com.slack.circuit.foundation.Circuit
import com.slack.circuit.foundation.LocalCircuit
import com.slack.circuit.foundation.NavigableCircuitContent
import com.slack.circuit.runtime.screen.PopResult
import com.slack.circuit.runtime.screen.Screen
import com.slack.circuitx.navigation.intercepting.CircuitInterceptingNavigator.FailureNotifier
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf

/**
* An implementation of [NavigableCircuitContent] that allows for a [CircuitNavigationInterceptor]
* to intercept navigation.
*
* @param onRootPop Handle the root pop. On Android this is handled by the system back handler if
* left as null.
*/
@Composable
public fun CircuitInterceptingNavigableContent(
screens: ImmutableList<Screen>,
interceptors: ImmutableList<CircuitNavigationInterceptor>,
modifier: Modifier = Modifier,
eventListeners: ImmutableList<CircuitNavigationEventListener> = persistentListOf(),
notifier: FailureNotifier? = null,
onRootPop: ((result: PopResult?) -> Unit)? = null,
circuit: Circuit = requireNotNull(LocalCircuit.current),
decoration: NavDecoration = circuit.defaultNavDecoration,
unavailableRoute: (@Composable (screen: Screen, modifier: Modifier) -> Unit) =
circuit.onUnavailableContent,
) {
check(screens.isNotEmpty()) { "No screens were provided." }
val backStack = rememberSaveableBackStack(screens)
// Build the delegate Navigator.
val interceptingNavigator =
rememberCircuitInterceptingNavigator(
backStack = backStack,
interceptors = interceptors,
eventListeners = eventListeners,
notifier = notifier,
onRootPop = onRootPop,
)
NavigableCircuitContent(
navigator = interceptingNavigator,
backStack = backStack,
modifier = modifier,
decoration = decoration,
unavailableRoute = unavailableRoute,
)
}
Loading