diff --git a/changelog/index.html b/changelog/index.html index 1364b4204..12a64a9c0 100644 --- a/changelog/index.html +++ b/changelog/index.html @@ -1859,6 +1859,7 @@
2.8.1
.1.7.0
.Special thanks to @chrisbanes, @adamp, and Chuck Jazdzewski for contributing to this release and helping us find a runtime fix for the pausableState
issue!
2024-05-28
Circuit is used in production at Slack and ready for general use \ud83d\ude80. The API is considered unstable as we continue to iterate on it.
"},{"location":"#overview","title":"Overview","text":"Circuit is a simple, lightweight, and extensible framework for building Kotlin applications that\u2019s Compose from the ground up.
Compose Runtime vs. Compose UI
Compose itself is essentially two libraries \u2013 Compose Compiler and Compose UI. Most folks usually think of Compose UI, but the compiler (and associated runtime) are actually not specific to UI at all and offer powerful state management APIs.
Jake Wharton has an excellent post about this: https://jakewharton.com/a-jetpack-compose-by-any-other-name/
It builds upon core principles we already know like Presenters and UDF, and adds native support in its framework for all the other requirements we set out for above. It\u2019s heavily influenced by Cash App\u2019s Broadway architecture (talked about at Droidcon NYC, also very derived from our conversations with them).
Circuit\u2019s core components are its Presenter
and Ui
interfaces.
Presenter
and a Ui
cannot directly access each other. They can only communicate through state and event emissions.Presenter
and Ui
each have a single composable function.Presenter
and Ui
are both generic types, with generics to define the UiState
types they communicate with.Screen
s. One runs a new Presenter
/Ui
pairing by requesting them with a given Screen
that they understand.Screens
The pairing of a Presenter
and Ui
for a given Screen
key is what we semantically call a \u201cscreen\u201d.
Presenter
+ Ui
pairing would be a \u201ccounter screen\u201d.Circuit\u2019s repo (https://github.com/slackhq/circuit) is being actively developed in the open, which allows us to continue collaborating with external folks too. We have a trivial-but-not-too-trivial sample app that we have been developing in it to serve as a demo for a number of common patterns in Circuit use.
"},{"location":"#counter-example","title":"Counter Example","text":"This is a very simple case of a Counter screen that displays the count and has buttons to increment and decrement.
There\u2019s some glue code missing from this example that\u2019s covered in the Code Gen section later.
@Parcelize\ndata object CounterScreen : Screen {\n data class CounterState(\n val count: Int,\n val eventSink: (CounterEvent) -> Unit,\n ) : CircuitUiState\n sealed interface CounterEvent : CircuitUiEvent {\n data object Increment : CounterEvent\n data object Decrement : CounterEvent\n }\n}\n\n@CircuitInject(CounterScreen::class, AppScope::class)\n@Composable\nfun CounterPresenter(): CounterState {\n var count by rememberSaveable { mutableStateOf(0) }\n\n return CounterState(count) { event ->\n when (event) {\n CounterEvent.Increment -> count++\n CounterEvent.Decrement -> count--\n }\n }\n}\n\n@CircuitInject(CounterScreen::class, AppScope::class)\n@Composable\nfun Counter(state: CounterState) {\n Box(Modifier.fillMaxSize()) {\n Column(Modifier.align(Alignment.Center)) {\n Text(\n modifier = Modifier.align(CenterHorizontally),\n text = \"Count: ${state.count}\",\n style = MaterialTheme.typography.displayLarge\n )\n Spacer(modifier = Modifier.height(16.dp))\n Button(\n modifier = Modifier.align(CenterHorizontally),\n onClick = { state.eventSink(CounterEvent.Increment) }\n ) { Icon(rememberVectorPainter(Icons.Filled.Add), \"Increment\") }\n Button(\n modifier = Modifier.align(CenterHorizontally),\n onClick = { state.eventSink(CounterEvent.Decrement) }\n ) { Icon(rememberVectorPainter(Icons.Filled.Remove), \"Decrement\") }\n }\n }\n}\n
"},{"location":"#license","title":"License","text":"Copyright 2022 Slack Technologies, LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n
"},{"location":"changelog/","title":"Changelog","text":""},{"location":"changelog/#unreleased","title":"Unreleased","text":""},{"location":"changelog/#0222","title":"0.22.2","text":"2024-06-04
pausableState
recomposition loops by avoiding backward snapshot writes.Circuit.presentWithLifecycle
flag to enable/disable automatic pausableState
use. This is enabled by default.1.6.11
.2.8.1
.1.7.0
.2024-05-28
rememberRetained
implicitly requiring LocalContext
where it used to no-op.2.0.0
.2024-05-28
2.0.0
.2.0.0-1.0.21
.1.6.10
.This release is otherwise identical to 0.21.0
, just updated to Kotlin 2.0.
2024-05-28
FakeNavigator
functions to check for the lack of pop/resetRoot events.FakeNavigator
constructor param to add additional screens to the backstack.StaticScreen
interface. When a StaticScreen
is used, Circuit will internally allow the UI to run on its own and won\u2019t connect it to a presenter if no presenter is provided.RecordLifecycle
and LocalRecordLifecycle
composition local, allowing UIs and presenters to observe when they are \u2018active\u2019. Currently, a record is considered \u2018active\u2019 when it is the top record on the back stack.rememberRetainedSaveable
variant that participates in both RetainedStateRegistry
and SaveableStateRegistry
restoration, allowing layered state persistence.rememberRetainedSaveable
entering composition:RetainedStateRegistry
and SaveableStateRegistry
, if availablerememberRetained
that explicitly requires a Saver
parameter.CircuitUiState
when they are not active. Presenters can opt-out of this behavior by implementing NonPausablePresenter
.NavigatorImpl.goTo
no longer navigates if the Screen
is equal to Navigator.peek()
.Presenter.present
is now annotated with @ComposableTarget(\"presenter\")
. This helps prevent use of Compose UI in the presentation logic as the compiler will emit a warning if you do. Note this does not appear in the IDE, so it\u2019s recommended to use allWarningsAsErrors
to fail the build on this event.Navigator.goTo()
calls to the same current screen.Navigator.goTo
now returns a Bool indicating navigation success.GestureNavigationDecoration
impl to commonMain
and rename to CupertinoGestureNavigationDecoration
.1.8
in core libraries.FakeNavigator.assertIsEmpty
and expectNoEvents
(use the specific event type methods instead)Presenter.Factory
as @Stable
.Ui.Factory
as @Stable
.CircuitContext
as @Stable
.EventListener
as @Stable
.EventListener.Factory
as @Stable
.1.9.24
.1.9.24-2.0.20
.1.5.14
.1.17.0
.2.8.0
.1.4.3
.1.8.0
.1.8.1
.1.6.2
.1.6.7
.1.6.7
.1.6.7
.1.6.7
.1.13.1
.1.9.0
.2.51.1
.0.8.4
.Special thanks to @chrisbanes, @alexvanyo, @eboudrant, @edenman, and @JustinBis for contributing to this release!
"},{"location":"changelog/#0200","title":"0.20.0","text":"2024-03-18
RememberObserver
to work with rememberRetained
.Navigator.popRoot()
. extension (#1274)CircuitContent
to keep Ui
and Presenter
consistent. We already did this for presenters, this just makes it consistent for both.ToastEffect
.rememberImpressionNavigator()
not delegating PopResult
.PopResult
to onRootPop()
.canRetainCheck
when saving RetainedStateRegistry
.com.google.guava:listenablefuture
to 1.0
to avoid conflicts with Guava.1.5.10.1
.1.8.0
.1.6.1
.1.6.3
.1.4.1
.2.51
.1.1.0
.0.8.3
.1.9.23
.1.9.23-1.0.19
.Special thanks to @chrisbanes, @aschulz90, and @alexvanyo for contributing to this release!
"},{"location":"changelog/#0191","title":"0.19.1","text":"2024-02-12
This is a small bug fix release focused SaveableBackStack
consistency and FakeNavigator
API improvements.
FakeNavigator.awaitNextScreen()
not suspending.FakeNavigator.resetRoot()
not returning the actual popped screens.Navigator.peekBackStack()
and Navigator.resetRoot()
return ImmutableList
.BackStack.popUntil()
return the ImmutableList
of the popped records.FakeNavigator.peekBackStack()
return the ImmutableList
of the popped records.FakeNavigator
. This should offer much more information about the events.BackStack
instance in FakeNavigator
+ allow for specifying a user-provided instance.FakeNavigator
unless using a custom BackStack
.goTo
event.rememberSaveableBackStack()
.Navigator()
factory function.2024-02-09
"},{"location":"changelog/#navigation-with-results","title":"Navigation with results","text":"This release introduces support for inter-screen navigation results. This is useful for scenarios where you want to pass data back to the previous screen after a navigation event, such as when a user selects an item from a list and you want to pass the selected item back to the previous screen.
var photoUrl by remember { mutableStateOf<String?>(null) }\nval takePhotoNavigator = rememberAnsweringNavigator<TakePhotoScreen.Result>(navigator) { result ->\n photoUrl = result.url\n}\n\n// Elsewhere\ntakePhotoNavigator.goTo(TakePhotoScreen)\n\n// In TakePhotoScreen.kt\ndata object TakePhotoScreen : Screen {\n @Parcelize\n data class Result(val url: String) : PopResult\n}\n\nclass TakePhotoPresenter {\n @Composable fun present(): State {\n // ...\n navigator.pop(result = TakePhotoScreen.Result(newFilters))\n }\n}\n
See the new section in the navigation docs for more details, as well as updates to the Overlays docs that help explain when to use an Overlay
vs navigating to a Screen
with a result.
This release introduces support for saving/restoring navigation state on root resets (aka multi back stack). This is useful for scenarios where you want to reset the back stack to a new root but still want to retain the previous back stack\u2019s state, such as an app UI that has a persistent bottom navigation bar with different back stacks for each tab.
This works by adding two new optional saveState
and restoreState
parameters to Navigator.resetRoot()
.
navigator.resetRoot(HomeNavTab1, saveState = true, restoreState = true)\n// User navigates to a details screen\nnavigator.push(EntityDetails(id = foo))\n// Later, user clicks on a bottom navigation item\nnavigator.resetRoot(HomeNavTab2, saveState = true, restoreState = true)\n// Later, user switches back to the first navigation item\nnavigator.resetRoot(HomeNavTab1, saveState = true, restoreState = true)\n// The existing back stack is restored, and EntityDetails(id = foo) will be top of\n// the back stack\n
There are times when saving and restoring the back stack may not be appropriate, so use this feature only when it makes sense. A common example where it probably does not make sense is launching screens which define a UX flow which has a defined completion, such as onboarding.
"},{"location":"changelog/#new-tutorial","title":"New Tutorial!","text":"On top of Circuit\u2019s existing docs, we\u2019ve added a new tutorial to help you get started with Circuit. It\u2019s a step-by-step guide that walks you through building a simple inbox app using Circuit, intended to serve as a sort of small code lab that one could do in 1-2 hours. Check it out here.
"},{"location":"changelog/#overlay-improvements","title":"Overlay Improvements","text":"AlertDialogOverlay
, BasicAlertDialogOverlay
, and BasicDialogOverlay
to circuitx-overlay
.OverlayEffect
to circuit-overlay
. This offers a simple composable effect to show an overlay and await a result. OverlayEffect(state) { host ->\n val result = host.show(AlertDialogOverlay(...))\n // Do something with the result\n}\n
OverlayState
and LocalOverlayState
to circuit-overlay
. This allows you to check the current overlay state (UNAVAILABLE
, HIDDEN
, or SHOWING
).OverlayHost
as @ReadOnlyOverlayApi
to indicate that it\u2019s not intended for direct implementation by consumers.Overlay
as @Stable
.NavEvent.screen
public.Navigator.popUntil
to be exclusive.Navigator.peek()
to peek the top screen of the back stack.Navigator.peekBackStack()
to peek the top screen of the back stack.backStack
.BackStack.Record
as @Stable
.onRootPop
of the Android rememberCircuitNavigator
.2.7.0
.1.5.12
.1.6.1
.2024.02.00
.1.5.9
.0.23.2
.2.4.9
.1.16.0
.1.9.22-1.0.17
.Special thanks to @milis92, @ChrisBanes, and @vulpeszerda for contributing to this release!
"},{"location":"changelog/#0182","title":"0.18.2","text":"2024-01-05
Record
s\u2019 ViewModelStores
. This fully fixes #1065.1.3.2
.1.5.7.1
.Special thanks to @dandc87 for contributing to this release!
"},{"location":"changelog/#0181","title":"0.18.1","text":"2024-01-01
ProvidedValues
lifetime. See #1065 for more details.GestureNavDecoration
dropping saveable/retained state on back gestures. See #1089 for more details.Special thanks to @ChrisBanes and @dandc87 for contributing to this release!
"},{"location":"changelog/#0180","title":"0.18.0","text":"2023-12-29
AnimatedOverlay
.ModalBottomSheet
appearance in BottomSheetOverlay
.1.9.22
.1.9.22-1.0.16
.2.50
.0.3.7
.1.8.2
.Special thanks to @ChrisBanes, @chriswiesner, and @BryanStern for contributing to this release!
"},{"location":"changelog/#0171","title":"0.17.1","text":"2023-12-05
SaveableStateRegistryBackStackRecordLocalProvider
to be supported across all currently supported platforms.LocalBackStackRecordLocalProviders
always returning a new composition local.androidx.compose.compiler:compiler
to 1.5.5
1.15.3
2.49
Special thanks to @alexvanyo for contributing to this release.
"},{"location":"changelog/#0170","title":"0.17.0","text":"2023-11-28
"},{"location":"changelog/#new-circuitx-effects-artifact","title":"New: circuitx-effects artifact","text":"The circuitx-effects artifact provides some effects for use with logging/analytics. These effects are typically used in Circuit presenters for tracking impressions
and will run only once until forgotten based on the current circuit-retained strategy.
dependencies {\n implementation(\"com.slack.circuit:circuitx-effects:<version>\")\n}\n
Docs: https://slackhq.github.io/circuit/circuitx/#effects
"},{"location":"changelog/#new-add-codegen-mode-to-support-both-anvil-and-hilt","title":"New: Add codegen mode to support both Anvil and Hilt","text":"Circuit\u2019s code gen artifact now supports generating for Hilt projects. See the docs for usage instructions: https://slackhq.github.io/circuit/code-gen/
"},{"location":"changelog/#misc_1","title":"Misc","text":"CircuitContent
internals like rememberPresenter()
, rememberUi
, etc for reuse.CircuitContent()
overload that accepts a pre-constructed presenter/ui parameters public to allow for more control over content.0.8.2
.1.15.1
.1.5.11
.1.9.21
.1.9.21-1.0.15
.1.5.4
.1.3.1
.Special thanks to @jamiesanson, @frett, and @bryanstern for contributing to this release!
"},{"location":"changelog/#0161","title":"0.16.1","text":"2023-11-09
1.9.20-1.0.14
.2023-11-01
circut-retained
is now enabled automatically in CircuitCompositionLocals
by default, we still allowing overriding it with no-op implementation.1.9.20
.1.5.2
.agp
to 8.1.2
.androidx.activity
to 1.8.0
.benchmark
to 1.2.0
.coil
to 2.5.0
.compose.material3
to 1.1.2
.compose.material
to 1.5.4
.compose.runtime
to 1.5.4
.compose.ui
to 1.5.4
.roborazzi
to 1.6.0
.2023-09-20
"},{"location":"changelog/#new-allow-retained-state-to-be-retained-whilst-uis-and-presenters-are-on-the-back-stack","title":"New: Allow retained state to be retained whilst UIs and Presenters are on the back stack.","text":"Originally, circuit-retained
was implemented as a solution for preserving arbitrary data across configuration changes on Android. With this change it now also acts as a solution for retaining state across the back stack, meaning that traversing the backstack no longer causes restored contents to re-run through their empty states anymore.
To support this, each back stack entry now has its own RetainedStateRegistry
instance.
Note that circuit-retained
is still optional for now, but we are considering making it part of CircuitCompositionLocals
in the future. Please let us know your thoughts in this issue: https://github.com/slackhq/circuit/issues/891.
Full details + demos can be found in https://github.com/slackhq/circuit/pull/888. Big thank you to @chrisbanes for the implementation!
"},{"location":"changelog/#other-changes","title":"Other changes","text":"collectAsRetainedState
utility function, analogous to collectAsState
but will retain the previous value across configuration changes and back stack entries.rememberRetained
with a port of the analogous optimization in rememberSaveable
. See #850.Presenter
and Ui
interfaces are now annotated as @Stable
.GestureNavigationDecoration
function parameter order.BackHandler
on iOS now has the proper file name.presenter.present()
in CircuitContent
on the Screen
rather than the presenter
itself, which fixes a severe issue that prevented currentCompositeKeyHash
from working correctly on rememberRetained
and rememberSaveable
uses.1.5.2
.1.5.1
.androidx.compose.animation
to 1.5.1
.androidx.compose.foundation
to 1.5.1
.androidx.compose.runtime
to 1.5.1
.androidx.compose.material
to 1.5.1
.androidx.lifecycle
to 2.6.2
.androidx.annotation
to 1.7.0
.2023-09-03
GestureNavigationDecoration
to CircuitX
courtesy of @chrisbanes.This is a new NavDecoration
that allows for gesture-based navigation, such as predictive back in Android 14 or drag gestures in iOS. See the docs for more details.
NavigableCircuitContent(\n navigator = navigator,\n backstack = backstack,\n decoration = GestureNavigationDecoration(\n // Pop the back stack once the user has gone 'back'\n navigator::pop\n )\n)\n
Special thanks to @chrisbanes and @alexvanyo for contributing to this release!
"},{"location":"changelog/#0140","title":"0.14.0","text":"2023-08-30
Overlay
types or Android navigation interop. See the docs for more details.Screen
to its own artifact. This is now under the com.slack.circuit.runtime.screen.Screen
name.Screen
directly in the BackStack
in place of route
.SaveableBackStack
in NavigableCircuitContent
, now any BackStack
impl is supported.CanRetainChecker
more customizable in circuit-retained
.DecoratedContent
, allowing more complex handling of back gestures (predictive back in android, drag gestures in iOS, etc).buildCircuitContentProviders()
in NavigableCircuitContent
, which enables movableContentOf
to work since it\u2019s reusing the same instance for records across changes.Modifier
for DecoratedContent
.circuit-test
artifact.kotlinx.collections.immutable
to core APIs.1.5.0
.1.5.3
.1.5.0
.1.5.0
.1.5.0
.1.5.0
.0.8.1
.1.2.0
.1.9.10
.1.9.10-1.0.13
.Thanks to @chrisbanes and @ashdavies for contributing to this release!
"},{"location":"changelog/#0130-beta01","title":"0.13.0-beta01","text":"2023-08-17
Overlay
types or Android navigation interop. See the docs for more details.circuit-test
artifact.1.5.0-beta02
.1.5.0
.1.5.0
.1.5.0
.1.5.0
.1.2.0
.1.9.0-1.0.13
.Note this release is a beta release due to the dependency on CM 1.5.0-beta02
.
2023-08-01
2.4.7
.2023-07-28
CircuitConfig
-> Circuit
. There is a source-compatible typealias for CircuitConfig
left with a deprecation replacement to ease migration.CircuitContext.config
-> CircuitContext.circuit
. The previous CircuitContext.config
function is left with a deprecation replacement to ease migration.TestEventSink
helper for testing event emissions in UI tests.1.9.0
.1.9.0-1.0.12
.1.4.3
.1.7.3
.1.5.1
(androidx) and 1.5.0
(compose-multiplatform).0.8.0
.2023-07-20
EventListener.start()
callback.1.0.0
.Thanks to @bryanstern for contributing to this release!
"},{"location":"changelog/#0101","title":"0.10.1","text":"2023-07-09
CircuitContent
overload with Navigator
public.Presenter
and Ui
in CircuitContent
.RememberRetained
.Special thanks to @chrisbanes and @bryanstern for contributing to this release!
"},{"location":"changelog/#0100","title":"0.10.0","text":"2023-06-30
RetainedStateRegistry
instances.0.11.0
.1.4.8
.1.4.1
.1.7.2
.1.0.0
.1.8.22
.Special thanks to @bryanstern, @saket, and @chrisbanes for contributing to this release!
"},{"location":"changelog/#091","title":"0.9.1","text":"2023-06-02
NavEvent
subtypes to public API.com.benasher44:uuid
to 0.7.1
.2.4.6
.2023-05-26
"},{"location":"changelog/#preliminary-support-for-ios-targets","title":"Preliminary support for iOS targets","text":"Following the announcement of Compose for iOS alpha, this release adds ios()
and iosSimulatorArm64()
targets for the Circuit core artifacts. Note that this support doesn\u2019t come with any extra APIs yet for iOS, just basic target support only. We\u2019re not super sure what direction we want to take with iOS, but encourage others to try it out and let us know what patterns you like. We have updated the Counter sample to include an iOS app target as well, using Circuit for the presentation layer only and SwiftUI for the UI.
Note that circuit-codegen and circuit-codegen-annotations don\u2019t support these yet, as Anvil and Dagger only support JVM targets.
More details can be found in the PR: https://github.com/slackhq/circuit/pull/583
"},{"location":"changelog/#misc_2","title":"Misc","text":"Note that we unintentionally used an experimental animation API for NavigatorDefaults.DefaultDecotration
, which may cause R8 issues if you use a newer, experimental version of Compose animation. To avoid issues, copy the animation code and use your own copy compiled against the newest animation APIs. We\u2019ll fix this after Compose 1.5.0 is released.
androidx.activity -> 1.7.2\ncompose -> 1.4.3\ncompose-compiler -> 1.4.7\ncoroutines -> 1.7.1\nkotlin -> 1.8.21\nkotlinpoet -> 1.13.2\nturbine -> 0.13.0\n
"},{"location":"changelog/#080","title":"0.8.0","text":"2023-04-06
"},{"location":"changelog/#core-split-up-core-artifacts","title":"[Core] Split up core artifacts.","text":"circuit-runtime
: common runtime components like Screen
, Navigator
, etc.circuit-runtime-presenter
: the Presenter
API, depends on circuit-runtime
.circuit-runtime-ui
: the Ui
API, depends on circuit-runtime
.circuit-foundation
: the circuit foundational APIs like CircuitConfig
, CircuitContent
, etc. Depends on the first three.The goal in this is to allow more granular dependencies and easier building against subsets of the API. For example, this would allow a presenter implementation to easily live in a standalone module that doesn\u2019t depend on any UI dependencies. Vice versa for UI implementations.
Where we think this could really shine is in multiplatform projects where Circuit\u2019s UI APIs may be more or less abstracted away in service of using native UI, like in iOS.
"},{"location":"changelog/#circuit-runtime-artifact","title":"circuit-runtime
artifact","text":"Before After com.slack.circuit.CircuitContext com.slack.circuit.runtime.CircuitContext com.slack.circuit.CircuitUiState com.slack.circuit.runtime.CircuitUiState com.slack.circuit.CircuitUiEvent com.slack.circuit.runtime.CircuitUiEvent com.slack.circuit.Navigator com.slack.circuit.runtime.Navigator com.slack.circuit.Screen com.slack.circuit.runtime.Screen"},{"location":"changelog/#circuit-runtime-presenter-artifact","title":"circuit-runtime-presenter
artifact","text":"Before After com.slack.circuit.Presenter com.slack.circuit.runtime.presenter.Presenter"},{"location":"changelog/#circuit-runtime-ui-artifact","title":"circuit-runtime-ui
artifact","text":"Before After com.slack.circuit.Ui com.slack.circuit.runtime.presenter.Ui"},{"location":"changelog/#circuit-foundation-artifact","title":"circuit-foundation
artifact","text":"Before After com.slack.circuit.CircuitCompositionLocals com.slack.circuit.foundation.CircuitCompositionLocals com.slack.circuit.CircuitConfig com.slack.circuit.foundation.CircuitConfig com.slack.circuit.CircuitContent com.slack.circuit.foundation.CircuitContent com.slack.circuit.EventListener com.slack.circuit.foundation.EventListener com.slack.circuit.NavEvent com.slack.circuit.foundation.NavEvent com.slack.circuit.onNavEvent com.slack.circuit.foundation.onNavEvent com.slack.circuit.NavigableCircuitContent com.slack.circuit.foundation.NavigableCircuitContent com.slack.circuit.NavigatorDefaults com.slack.circuit.foundation.NavigatorDefaults com.slack.circuit.rememberCircuitNavigator com.slack.circuit.foundation.rememberCircuitNavigator com.slack.circuit.push com.slack.circuit.foundation.push com.slack.circuit.screen com.slack.circuit.foundation.screen"},{"location":"changelog/#more-highlights","title":"More Highlights","text":"NavigableCircuitContent
and just use common one. Back handling still runs through BackHandler
, but is now configured in rememberCircuitNavigator
.defaultNavDecoration
to CircuitConfig
to allow for customizing the default NavDecoration
used in NavigableCircuitContent
.CircuitUiState
as @Stable
instead of @Immutable
.:samples:tacos
order builder sample to demonstrate complex state management.NavigableCircuitContent
example in the desktop counter.1.4.1
.1.4.4
.1.7.0
.0.7.1
.2023-02-10
NavigableCircuitContent
! Special thanks to @ashdavies for contributions to make this possible.circuit-retained
minSdk is now 21 again. We accidentally bumped it to 28 when merging in its instrumentation tests.circuit-core
artifact.circuit-retained
is now covered in embedded baseline profiles.2.45
.1.8.10-1.0.9
.1.4.2
.1.8.10
.2023-02-02
Happy groundhog day!
Ui.Content()
now contains a Modifier
parameter.This allows you to pass modifiers on to UIs directly.
public interface Ui<UiState : CircuitUiState> {\n- @Composable public fun Content(state: UiState)\n+ @Composable public fun Content(state: UiState, modifier: Modifier)\n }\n
Navigator.resetRoot(Screen)
function to reset the backstack root with a new root screen. There is a corresponding awaitResetRoot()
function added to FakeNavigator
.EventListener.start
callback function.Modifier
in the API).CircuitContext.putTag
generics.EventListener.onState
\u2019s type is now CircuitUiState
instead of Any
.ScreenUi
is now removed and Ui.Factory
simply returns Ui
instances now.API Change: CircuitConfig.onUnavailableContent
is now no longer nullable. By default it displays a big ugly error text. If you want the previous behavior of erroring, replace it with a composable function that just throws an exception.
Dependency updates
Kotlin 1.8.0\nCompose-JB 1.3.0\nKSP 1.8.0-1.0.9\nCompose Runtime 1.3.3\nCompose UI 1.3.3\nCompose Animation 1.3.3\n
2022-12-22
ViewModel
s. This is now done automatically by the Circuit itself.circuit-retained
is now fully optional and not included as a transitive dependency of circuit-core. If you want to use it, see its installation instructions in its README.Screen
as @Immutable
.LocalCircuitOwner
is now just LocalCircuitConfig
to be more idiomatic.LocalRetainedStateRegistryOwner
is now just LocalRetainedStateRegistry
to be more idiomatic.Continuity
is now internal
and not publicly exposed since it no longer needs to be manually provided.ViewModelBackStackRecordLocalProvider
is now internal
and not publicly exposed since it no longer needs to be manually provided.[versions]\nanvil = \"2.4.3\"\ncompose-jb = \"1.2.2\"\ncompose-animation = \"1.3.2\"\ncompose-compiler = \"1.3.2\"\ncompose-foundation = \"1.3.1\"\ncompose-material = \"1.3.1\"\ncompose-material3 = \"1.0.1\"\ncompose-runtime = \"1.3.2\"\ncompose-ui = \"1.3.2\"\nkotlin = \"1.7.22\"\n
2022-12-07
Presenter
and Ui
factories\u2019 create()
functions now offer a CircuitContext
parameter in place of a CircuitConfig
parameter. This class contains a CircuitConfig
, a tagging API, and access to parent contexts. This allows for plumbing your own metadata through Circuit\u2019s internals such as tracing tools, logging, etc.EventListener
.onBeforeCreatePresenter
onAfterCreatePresenter
onBeforeCreateUi
onAfterCreateUi
onUnavailableContent
onStartPresent
onDisposePresent
onStartContent
onDisposeContent
dispose
1.3.1
.1.2.1
.0.6.1
.2022-11-07
onRootPop()
parameter in rememberCircuitNavigator()
but use LocalOnBackPressedDispatcherOwner
for backpress handling by default.2022-11-01
circuit-overlay
artifact.circuit-core
artifact now packages in baseline profiles.onRootPop()
option in rememberCircuitNavigator()
, instead you should install your own BackHandler()
prior to rendering your circuit content to customize back behavior when the circuit Navigator
is at root.circuit-codegen-annotations
is now a multiplatform project and doesn\u2019t accidentally impose the compose-desktop dependency.We\u2019ve also updated a number of docs around code gen, overlays, and interop (including a new interop sample).
"},{"location":"changelog/#022","title":"0.2.2","text":"2022-10-27
2022-10-27
2022-10-26
New: Code gen artifact. This targets specifically using Dagger + Anvil and will generate Presenter
and Ui.Factory
implementations for you. See CircuitInject
for more details.
ksp(\"com.slack.circuit:circuit-codegen:x.y.z\")\nimplementation(\"com.slack.circuit:circuit-codegen-annotations:x.y.z\")\n
New: There is now an EventListener
API for instrumenting state changes for a given Screen
. See its docs for more details.
rememberRetained
implementation and support for multiple variables. Previously it only worked with one variable.Dependency updates
androidx.activity 1.6.1\nandroidx.compose 1.3.0\nMolecule 0.5.0\n
"},{"location":"changelog/#012","title":"0.1.2","text":"2022-10-12
1.2.0
.0.12.0
.Presenter.test()
.2022-10-10
2022-10-10
Initial release, see the docs: https://slackhq.github.io/circuit/.
Note that this library is still under active development and not recommended for production use. We\u2019ll do a more formal announcement when that time comes!
"},{"location":"circuit-content/","title":"CircuitContent","text":"The simplest entry point of a Circuit screen is the composable CircuitContent
function. This function accepts a Screen
and automatically finds and pairs corresponding Presenter
and Ui
instances to render in it.
CircuitCompositionLocals(circuit) {\n CircuitContent(HomeScreen)\n}\n
This can be used for simple screens or as nested components of larger, more complex screens.
"},{"location":"circuitx/","title":"CircuitX","text":"CircuitX is a suite of extension artifacts for Circuit. These artifacts are intended to be batteries-included implementations of common use cases, such as out-of-the-box Overlay
types or Android navigation interop.
These packages differ from Circuit\u2019s core artifacts in a few ways:
com.slack.circuitx
package prefix.The circuitx-android
artifact contains Android-specific extensions for Circuit.
dependencies {\n implementation(\"com.slack.circuit:circuitx-android:<version>\")\n}\n
"},{"location":"circuitx/#navigation","title":"Navigation","text":"It can be important for Circuit to be able to navigate to Android targets, such as other activities or custom tabs. To support this, decorate your existing Navigator
instance with rememberAndroidScreenAwareNavigator()
.
class MainActivity : Activity {\n override fun onCreate(savedInstanceState: Bundle?) {\n setContent {\n val backStack = rememberSaveableBackStack(root = HomeScreen)\n val navigator = rememberAndroidScreenAwareNavigator(\n rememberCircuitNavigator(backstack), // Decorated navigator\n this@MainActivity\n )\n CircuitCompositionLocals(circuit) {\n NavigableCircuitContent(navigator, backstack)\n }\n }\n }\n}\n
rememberAndroidScreenAwareNavigator()
has two overloads - one that accepts a Context
and one that accepts an AndroidScreenStarter
. The former is just a shorthand for the latter that only supports IntentScreen
. You can also implement your own starter that supports other screen types.
AndroidScreen
is the base Screen
type that this navigator and AndroidScreenStarter
interact with. There is a built-in IntentScreen
implementation that wraps an Intent
and an options Bundle
to pass to startActivity()
. Custom AndroidScreens
can be implemented separately and route through here, but you should be sure to implement your own AndroidScreenStarter
to handle them accordingly.
CircuitX provides some effects for use with logging/analytics. These effects are typically used in Circuit presenters for tracking impressions
and will run only once until forgotten based on the current circuit-retained strategy.
dependencies {\n implementation(\"com.slack.circuit:circuitx-effects:<version>\")\n}\n
"},{"location":"circuitx/#impressioneffect","title":"ImpressionEffect","text":"ImpressionEffect
is a simple single fire side effect useful for logging or analytics. This impression
will run only once until it is forgotten based on the current RetainedStateRegistry
.
ImpressionEffect {\n // Impression \n}\n
"},{"location":"circuitx/#launchedimpressioneffect","title":"LaunchedImpressionEffect","text":"This is useful for async single fire side effects like logging or analytics. This effect will run a suspendable impression
once until it is forgotten based on the RetainedStateRegistry
.
LaunchedImpressionEffect {\n // Impression \n}\n
"},{"location":"circuitx/#rememberimpressionnavigator","title":"RememberImpressionNavigator","text":"A LaunchedImpressionEffect
that is useful for async single fire side effects like logging or analytics that need to be navigation aware. This will run the impression
again if it re-enters the composition after a navigation event.
val navigator = rememberImpressionNavigator(\n navigator = Navigator()\n) {\n // Impression\n}\n
"},{"location":"circuitx/#gesture-navigation","title":"Gesture Navigation","text":"CircuitX provides NavDecoration
implementation which support navigation through appropriate gestures on certain platforms.
dependencies {\n implementation(\"com.slack.circuit:circuitx-gesture-navigation:<version>\")\n}\n
To enable gesture navigation support, you can use the use the GestureNavigationDecoration
function:
NavigableCircuitContent(\n navigator = navigator,\n backStack = backstack,\n decoration = GestureNavigationDecoration(\n // Pop the back stack once the user has gone 'back'\n navigator::pop\n )\n)\n
"},{"location":"circuitx/#android_1","title":"Android","text":"On Android, this supports the Predictive back gesture which is available on Android 14 and later (API level 34+). On older platforms, Circuit\u2019s default NavDecoration
decoration is used instead.
On iOS, this simulates iOS\u2019s \u2018Interactive Pop Gesture\u2019 in Compose UI, allowing the user to swipe Circuit UIs away. As this is a simulation of the native behavior, it does not match the native functionality perfectly. However, it is a good approximation.
Tivi app running on iPhone"},{"location":"circuitx/#other-platforms","title":"Other platforms","text":"On other platforms we defer to Circuit\u2019s default NavDecoration
decoration.
CircuitX provides a few out-of-the-box Overlay
implementations that you can use to build common UIs.
dependencies {\n implementation(\"com.slack.circuit:circuitx-overlays:<version>\")\n}\n
"},{"location":"circuitx/#bottomsheetoverlay","title":"BottomSheetOverlay
","text":"BottomSheetOverlay
is an overlay that shows a bottom sheet with a strongly-typed API for the input model to the sheet content and result type. This allows you to easily use a bottom sheet to prompt for user input and suspend the underlying Circuit content until that result is returned.
/** A hypothetical bottom sheet of available actions when long-pressing a list item. */\nsuspend fun OverlayHost.showLongPressActionsSheet(): Action {\n return show(\n BottomSheetOverlay(\n model = listOfActions()\n ) { actions, overlayNavigator ->\n ActionsSheet(\n actions,\n overlayNavigator::finish // Finish the overlay with the clicked Action\n )\n }\n )\n}\n\n@Composable\nfun ActionsSheet(actions: List<Action>, onActionClicked: (Action) -> Unit) {\n Column {\n actions.forEach { action ->\n TextButton(onClick = { onActionClicked(action) }) {\n Text(action.title)\n }\n }\n }\n}\n
"},{"location":"circuitx/#dialog-overlays","title":"Dialog Overlays","text":"alertDialogOverlay
is function that returns an Overlay that shows a simple confirmation dialog with configurable inputs. This acts more or less as an Overlay
shim over the Material 3 AlertDialog
API.
/** A hypothetical confirmation dialog. */\nsuspend fun OverlayHost.showConfirmationDialog(): Action {\n return show(\n alertDialogOverlay(\n titleText = { Text(\"Are you sure?\") },\n confirmButton = { onClick -> Button(onClick = onClick) { Text(\"Yes\") } },\n dismissButton = { onClick -> Button(onClick = onClick) { Text(\"No\") } },\n )\n )\n}\n
There are also more generic BasicAlertDialog
and BasicDialog
implementations that allow more customization.
FullScreenOverlay
","text":"Sometimes it\u2019s useful to have a full-screen overlay that can be used to show a screen in full above the current content. This API is fairly simple to use and just takes a Screen
input of what content you want to show in the overlay.
overlayHost.showFullScreenOverlay(\n ImageViewerScreen(id = url, url = url, placeholderKey = name)\n)\n
When to use FullScreenOverlay
vs navigating to a Screen
?
While they achieve similar results, the key difference is that FullScreenOverlay
is inherently an ephemeral UI that is controlled by an underlying primary UI. It cannot navigate elsewhere and it does not participate in the backstack.
If using Dagger and Anvil or Hilt, Circuit offers a KSP-based code gen solution to ease boilerplate around generating factories.
"},{"location":"code-gen/#installation","title":"Installation","text":"plugins {\n id(\"com.google.devtools.ksp\")\n}\n\ndependencies {\n api(\"com.slack.circuit:circuit-codegen-annotations:<version>\")\n ksp(\"com.slack.circuit:circuit-codegen:<version>\")\n}\n
Note that Anvil is enabled by default. If you are using Hilt, you must specify the mode as a KSP arg.
ksp {\n arg(\"circuit.codegen.mode\", \"hilt\")\n}\n
If using Kotlin multiplatform with typealias annotations for Dagger annotations (i.e. expect annotations in common with actual typealias declarations in JVM source sets), you can match on just annotation short names alone to support this case via circuit.codegen.lenient
mode.
ksp {\n arg(\"circuit.codegen.lenient\", \"true\")\n}\n
"},{"location":"code-gen/#usage","title":"Usage","text":"The primary entry point is the CircuitInject
annotation.
This annotation is used to mark a UI or presenter class or function for code generation. When annotated, the type\u2019s corresponding factory will be generated and keyed with the defined screen
.
The generated factories are then contributed to Anvil via ContributesMultibinding
and scoped with the provided scope
key.
Presenter
and Ui
classes can be annotated and have their corresponding Presenter.Factory
or Ui.Factory
classes generated for them.
Presenter
@CircuitInject(HomeScreen::class, AppScope::class)\nclass HomePresenter @Inject constructor(...) : Presenter<HomeState> { ... }\n\n// Generates\n@ContributesMultibinding(AppScope::class)\nclass HomePresenterFactory @Inject constructor() : Presenter.Factory { ... }\n
UI
@CircuitInject(HomeScreen::class, AppScope::class)\nclass HomeUi @Inject constructor(...) : Ui<HomeState> { ... }\n\n// Generates\n@ContributesMultibinding(AppScope::class)\nclass HomeUiFactory @Inject constructor() : Ui.Factory { ... }\n
"},{"location":"code-gen/#functions","title":"Functions","text":"Simple functions can be annotated and have a corresponding Presenter.Factory
generated. This is primarily useful for simple cases where a class is just technical tedium.
Requirements - Presenter function names must end in Presenter
, otherwise they will be treated as UI functions. - Presenter functions must return a CircuitUiState
type. - UI functions can optionally accept a CircuitUiState
type as a parameter, but it is not required. - UI functions must return Unit
. - Both presenter and UI functions must be Composable
.
Presenter
@CircuitInject(HomeScreen::class, AppScope::class)\n@Composable\nfun HomePresenter(): HomeState { ... }\n\n// Generates\n@ContributesMultibinding(AppScope::class)\nclass HomePresenterFactory @Inject constructor() : Presenter.Factory { ... }\n
UI
@CircuitInject(HomeScreen::class, AppScope::class)\n@Composable\nfun Home(state: HomeState) { ... }\n*\n// Generates\n@ContributesMultibinding(AppScope::class)\nclass HomeUiFactory @Inject constructor() : Ui.Factory { ... }\n
"},{"location":"code-gen/#assisted-injection","title":"Assisted injection","text":"Any type that is offered in Presenter.Factory
and Ui.Factory
can be offered as an assisted injection to types using Dagger AssistedInject
. For these cases, the AssistedFactory
-annotated interface should be annotated with CircuitInject
instead of the enclosing class.
Types available for assisted injection are:
Screen
\u2013 the screen key used to create the Presenter
or Ui
.Navigator
\u2013 (presenters only)Circuit
Each should only be defined at-most once.
Examples
// Function example\n@CircuitInject(HomeScreen::class, AppScope::class)\n@Composable\nfun HomePresenter(screen: Screen, navigator: Navigator): HomeState { ... }\n\n// Class example\nclass HomePresenter @AssistedInject constructor(\n @Assisted screen: Screen,\n @Assisted navigator: Navigator,\n ...\n) : Presenter<HomeState> {\n // ...\n @CircuitInject(HomeScreen::class, AppScope::class)\n @AssistedFactory\n fun interface Factory {\n fun create(screen: Screen, navigator: Navigator, context: CircuitContext): HomePresenter\n }\n}\n
"},{"location":"code-of-conduct/","title":"Code of Conduct","text":""},{"location":"code-of-conduct/#introduction","title":"Introduction","text":"Diversity and inclusion make our community strong. We encourage participation from the most varied and diverse backgrounds possible and want to be very clear about where we stand.
Our goal is to maintain a safe, helpful and friendly community for everyone, regardless of experience, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, nationality, or other defining characteristic.
This code and related procedures also apply to unacceptable behavior occurring outside the scope of community activities, in all community venues (online and in-person) as well as in all one-on-one communications, and anywhere such behavior has the potential to adversely affect the safety and well-being of community members.
For more information on our code of conduct, please visit https://slackhq.github.io/code-of-conduct
"},{"location":"contributing/","title":"Contributors Guide","text":"Note that this project is considered READ-ONLY. You are welcome to discuss or ask questions in the discussions section of the repo, but we do not normally accept external contributions without prior discussion.
"},{"location":"contributing/#development","title":"Development","text":"Check out this repo with Android Studio. It\u2019s a standard gradle project and conventional to checkout.
Circuit is a Kotlin Multiplatform project, so ensure you have your environment set up accordingly: https://www.jetbrains.com/help/kotlin-multiplatform-dev/multiplatform-setup.html
The primary project is circuit
. The primary sample is samples/star
.
This project is written in Kotlin and should only use Kotlin.
Code formatting is checked via Spotless. To run the formatter, use the spotlessApply
command.
./gradlew spotlessApply\n
"},{"location":"contributing/#ios","title":"iOS","text":"To build any of the iOS checks, you must do the following: 1. Run bundle install
to set up fastlane. 2. Have swiftformat
installed. You can install it via brew install swiftformat
.
At its core, Circuit works on the Factory pattern. Every Presenter
and Ui
is contributed to a Circuit
instance by a corresponding factory that creates them for given Screen
s. These are intended to be aggregated in the DI layer and added to a Circuit
instance during construction.
val circuit = Circuit.Builder()\n .addUiFactory(FavoritesUiFactory())\n .addPresenterFactory(FavoritesPresenterFactory())\n .build()\n
Look familiar?
If you\u2019ve used Moshi or Retrofit, these should feel fairly familiar!
Presenter factories can be generated or hand-written, depending on if they aggregate an entire screen or are simple one-offs. Presenters are also given access to the current Navigator in this level.
class FavoritesScreenPresenterFactory @Inject constructor(\n private val favoritesPresenterFactory: FavoritesPresenter.Factory,\n) : Presenter.Factory {\n override fun create(screen: Screen, navigator: Navigator, context: CircuitContext): Presenter<*>? {\n return when (screen) {\n is FavoritesScreen -> favoritesPresenterFactory.create(screen, navigator, context)\n else -> null\n }\n }\n}\n
UI factories are similar, but generally should not aggregate other UIs unless there\u2019s a DI-specific reason to do so (which there usually isn\u2019t!).
class FavoritesScreenUiFactory @Inject constructor() : Ui.Factory {\n override fun create(screen: Screen, context: CircuitContext): Ui<*>? {\n return when (screen) {\n is FavoritesScreen -> favoritesUi()\n else -> null\n }\n }\n}\n\nprivate fun favoritesUi() = ui<State> { state, modifier -> Favorites(state, modifier) }\n
Info
Note how these include a Modifier
. You should pass on these modifiers to your UI. Always provide a modifier!
We canonically write these out as a separate function (favoritesUi()
) that returns a Ui
, which in turn calls through to the real (basic) Compose UI function (Favorites()
). This ensures our basic compose functions are top-level and accessible by tests, and also discourages storing anything in class members rather than idiomatic composable state vars. If you use code gen, it handles the intermediate function for you.
Circuit can interop anywhere that Compose can interop. This includes common cases like Android Views
, RxJava, Kotlin Flow
, and more.
Presenter
","text":"Lean on first-party interop-APIs where possible! See examples of interop with different libraries in the :samples:interop
project.
UI
","text":""},{"location":"interop/#ui-view","title":"Ui
-> View
","text":"Just embed the Circuit in a ComposeView
like any other Compose UI.
View
-> Ui
","text":"You can wrap your view in an AndroidView
in a custom Ui
implementation.
class ExistingCustomViewUi : Ui<State> {\n @Composable\n fun Content(state: State, modifier: Modifier = Modifier) {\n AndroidView(\n modifier = ...\n factory = { context ->\n ExistingCustomView(context)\n },\n update = { view ->\n view.setState(state)\n view.setOnClickListener { state.eventSink(Event.Click) }\n }\n }\n}\n
"},{"location":"navigation/","title":"Navigation","text":"For navigable contents, we have a custom compose-based backstack implementation that the androidx folks shared with us. Navigation becomes two parts:
BackStack
, where we use a SaveableBackStack
implementation that saves a stack of Screen
s and the ProvidedValues
for each record on that stack (allowing us to save and restore on configuration changes automatically).Navigator
, which is a simple interface that we can point at a BackStack
and offers simple goTo(<screen>)
/pop()
semantics. These are offered to presenters to perform navigation as needed to other screens.A new navigable content surface is handled via the NavigableCircuitContent
functions.
setContent {\n val backStack = rememberSaveableBackStack(root = HomeScreen)\n val navigator = rememberCircuitNavigator(backStack)\n NavigableCircuitContent(navigator, backStack)\n}\n
Warning
SaveableBackStack
must have a size of 1 or more after initialization. It\u2019s an error to have a backstack with zero items.
Presenters are then given access to these navigator instances via Presenter.Factory
(described in Factories), which they can save if needed to perform navigation.
fun showAddFavorites() {\n navigator.goTo(\n AddFavorites(\n externalId = uuidGenerator.generate()\n )\n )\n}\n
If you want to have custom behavior for when back is pressed on the root screen (i.e. backstack.size == 1
), you should implement your own BackHandler
and use it before creating the backstack.
setContent {\n val backStack = rememberSaveableBackStack(root = HomeScreen)\n BackHandler(onBack = { /* do something on root */ })\n // The Navigator's internal BackHandler will take precedence until it is at the root screen.\n val navigator = rememberCircuitNavigator(backstack)\n NavigableCircuitContent(navigator, backstack)\n}\n
"},{"location":"navigation/#results","title":"Results","text":"In some cases, it makes sense for a screen to return a result to the previous screen. This is done by using the an answering Navigator pattern in Circuit.
The primary entry point for requesting a result is the rememberAnsweringNavigator
API, which takes a Navigator
or BackStack
and PopResult
type and returns a navigator that can go to a screen and await a result.
Result types must implement PopResult
and are used to carry data back to the previous screen.
The returned navigator should be used to navigate to the screen that will return the result. The target screen can then pop
the result back to the previous screen and Circuit will automatically deliver this result to the previous screen\u2019s receiver.
var photoUri by remember { mutableStateOf<String?>(null) }\nval takePhotoNavigator = rememberAnsweringNavigator<TakePhotoScreen.Result>(navigator) { result ->\n photoUri = result.uri\n}\n\n// Elsewhere\ntakePhotoNavigator.goTo(TakePhotoScreen)\n\n// In TakePhotoScreen.kt\ndata object TakePhotoScreen : Screen {\n @Parcelize\n data class Result(val uri: String) : PopResult\n}\n\nclass TakePhotoPresenter {\n @Composable fun present(): State {\n // ...\n navigator.pop(result = TakePhotoScreen.Result(photoUri))\n }\n}\n
Circuit automatically manages saving/restoring result states and ensuring that results are only delivered to the original receiver that requested it. If the target screen does not pop back a result, the previous screen\u2019s receiver will just never receive one.
When to use an Overlay
vs navigating to a Screen
with result?
See this doc in Overlays!
"},{"location":"navigation/#nested-navigation","title":"Nested Navigation","text":"Navigation carries special semantic value in CircuitContent
as well, where it\u2019s common for UIs to want to curry navigation events emitted by nested UIs. For this case, there\u2019s a CircuitContent
overload that accepts an optional onNavEvent callback that you must then forward to a Navigator instance.
@Composable fun ParentUi(state: ParentState, modifier: Modifier = Modifier) {\n CircuitContent(NestedScreen, modifier = modifier, onNavEvent = { navEvent -> state.eventSink(NestedNav(navEvent)) })\n}\n\n@Composable fun ParentPresenter(navigator: Navigator): ParentState {\n return ParentState(...) { event ->\n when (event) {\n is NestedNav -> navigator.onNavEvent(event.navEvent)\n }\n }\n}\n\n@Composable \nfun NestedPresenter(navigator: Navigator): NestedState {\n // These are forwarded up!\n navigator.goTo(AnotherScreen)\n\n // ...\n}\n
"},{"location":"overlays/","title":"Overlays","text":"The circuit-overlay
artifact contains an optional API for presenting overlays on top of the current UI.
@Composable\nfun SubmitAnswer(state: FormState, modifier: Modifier = Modifier) {\n if (state.promptConfirmation) {\n OverlayEffect { host ->\n // Suspend on the result of the overlay\n val result = host.show(ConfirmationDialogOverlay(title = \"Are you sure?\"))\n state.eventSink(SubmitAnswerEvent(result))\n }\n }\n}\n
"},{"location":"overlays/#usage","title":"Usage","text":"The core APIs are the Overlay
and OverlayHost
interfaces.
An Overlay
is composable content that can be shown on top of other content via an OverlayHost
. Overlays are typically used for one-off request/result flows and should not usually attempt to do any sort of external navigation or make any assumptions about the state of the app. They should only emit a Result
to the given OverlayNavigator
parameter when they are done.
interface Overlay<Result : Any> {\n @Composable\n fun Content(navigator: OverlayNavigator<Result>)\n}\n
For common overlays, it\u2019s useful to create a common Overlay
subtype that can be reused. For example: BottomSheetOverlay
, ModalOverlay
, TooltipOverlay
, etc.
An OverlayHost
is provided via composition local and exposes a suspend show()
function to show an overlay and resume with a typed Result
.
val result = LocalOverlayHost.current.show(BottomSheetOverlay(...))\n
Where BottomSheetOverlay
is a custom bottom sheet implementation of an Overlay
.
In composition, you can also use OverlayEffect
for a more streamlined API.
OverlayEffect { host ->\n val result = host.show(BottomSheetOverlay(...))\n}\n
"},{"location":"overlays/#installation","title":"Installation","text":"Add the dependency.
implementation(\"com.slack.circuit:circuit-overlay:$circuit_version\")\n
The simplest starting point for adding overlay support is the ContentWithOverlays
composable function.
ContentWithOverlays {\n // Your content here\n}\n
This will expose a LocalOverlayHost
composition local that can be used by UIs to show overlays. This also exposes a LocalOverlayState
composition local that can be used to check the current overlay state (UNAVAILABLE
, HIDDEN
, or SHOWING
).
Overlay
vs PopResult
","text":"Overlays and navigation results can accomplish similar goals, and you should choose the right one for your use case.
Overlay
PopResult
Survives process death \u274c \u2705 Type-safe \u2705 \ud83d\udfe1 Suspend on result \u2705 \u274c Participates in back stack \u274c \u2705 Supports non-saveable inputs/outputs \u2705 \u274c Can participate with the caller\u2019s UI \u2705 \u274c Can return multiple different result types \u274c \u2705 Works without a back stack \u2705 \u274c *PopResult
is technically type-safe, but it\u2019s not as strongly typed as Overlay
results since there is nothing inherently requiring the target screen to pop a given result type back.
The core Presenter interface is this:
interface Presenter<UiState : CircuitUiState> {\n @ComposableTarget(\"presenter\")\n @Composable\n fun present(): UiState\n}\n
Presenters are solely intended to be business logic for your UI and a translation layer in front of your data layers. They are generally Dagger-injected types as the data layers they interpret are usually coming from the DI graph. In simple cases, they can be typed as a simple @Composable
presenter function allowing Circuit code gen to generate the corresponding interface and factory for you.
A very simple presenter can look like this:
class FavoritesPresenter(...) : Presenter<State> {\n @Composable override fun present(): State {\n var favorites by remember { mutableStateOf(<initial>) }\n\n return State(favorites) { event -> ... }\n }\n}\n
In this example, the present()
function simply computes a state
and returns it. If it has UI events to handle, an eventSink: (Event) -> Unit
property should be exposed in the State
type it returns.
With DI, the above example becomes something more like this:
class FavoritesPresenter @AssistedInject constructor(\n @Assisted private val screen: FavoritesScreen,\n @Assisted private val navigator: Navigator,\n private val favoritesRepository: FavoritesRepository\n) : Presenter<State> {\n @Composable override fun present(): State {\n // ...\n }\n @AssistedFactory\n fun interface Factory {\n fun create(screen: FavoritesScreen, navigator: Navigator, context: CircuitContext): FavoritesPresenter\n }\n}\n
Assisted injection allows passing on the screen
and navigator
from the relevant Presenter.Factory
to this presenter for further reference.
When dealing with nested presenters, a presenter could bypass implementing a class entirely by simply being written as a function that other presenters can use.
// From cashapp/molecule's README examples\n@Composable\nfun ProfilePresenter(\n userFlow: Flow<User>,\n balanceFlow: Flow<Long>,\n): ProfileModel {\n val user by userFlow.collectAsState(null)\n val balance by balanceFlow.collectAsState(0L)\n\n return if (user == null) {\n Loading\n } else {\n Data(user.name, balance)\n }\n}\n
Presenters can present other presenters by injecting their assisted factories/providers, but note that this makes them a composite presenter that is now assuming responsibility for managing state of multiple nested presenters.
"},{"location":"presenter/#no-compose-ui","title":"No Compose UI","text":"Presenter logic should not emit any Compose UI. They are purely for presentation business logic. To help enforce this, Presenter.present
is annotated with @ComposableTarget(\"presenter\")
. This helps prevent use of Compose UI in the presentation logic as the compiler will emit a warning if you do.
Tip
This warning does not appear in the IDE, so it\u2019s recommended to use allWarningsAsErrors
in your build configuration to fail the build on this event.
// In build.gradle.kts\nkotlin.compilerOptions.allWarningsAsErrors.set(true)\n
"},{"location":"presenter/#retention","title":"Retention","text":"There are three types of composable retention functions used in Circuit.
remember
\u2013 from Compose, remembers a value across recompositions. Can be any type.rememberRetained
\u2013 custom, remembers a value across recompositions, the back stack, and configuration changes. Can be any type, but should not retain leak-able things like Navigator
instances or Context
instances. Backed by a hidden ViewModel
on Android.rememberSaveable
\u2013 from Compose, remembers a value across recompositions, the back stack, configuration changes, and process death. Must be a primitive, Parcelable
(on Android), or implement a custom Saver
. This should not retain leakable things like Navigator
instances or Context
instances and is backed by the framework saved instance state system.Developers should use the right tool accordingly depending on their use case. Consider these three examples.
The first one will preserve the count
value across recompositions, but not the back stack, configuration changes, or process death.
@Composable\nfun CounterPresenter(): CounterState {\n var count by remember { mutableStateOf(0) }\n\n return CounterState(count) { event ->\n when (event) {\n is CounterEvent.Increment -> count++\n is CounterEvent.Decrement -> count--\n }\n }\n}\n
The second one will preserve the state across recompositions, the back stack, and configuration changes, but not process death.
@Composable\nfun CounterPresenter(): CounterState {\n var count by rememberRetained { mutableStateOf(0) }\n\n return CounterState(count) { event ->\n when (event) {\n is CounterEvent.Increment -> count++\n is CounterEvent.Decrement -> count--\n }\n }\n}\n
The third case will preserve the count
state across recompositions, the back stack, configuration changes, and process death.
@Composable\nfun CounterPresenter(): CounterState {\n var count by rememberSaveable { mutableStateOf(0) }\n\n return CounterState(count) { event ->\n when (event) {\n is CounterEvent.Increment -> count++\n is CounterEvent.Decrement -> count--\n }\n }\n}\n
remember
rememberRetained
rememberSaveable
Recompositions \u2705 \u2705 \u2705 Back stack \u274c \u2705* \u2705* Configuration changes (Android) \u274c \u2705 \u2705 Process death \u274c \u274c \u2705 Can be non-Saveable types \u2705 \u2705 \u274c *If using NavigableCircuitContent
\u2019s default configuration.
Screens are keys for Presenter and UI pairings.
The core Screen
interface is this:
interface Screen : Parcelable\n
These types are Parcelable
on Android for saveability in our backstack and easy deeplinking. A Screen
can be a simple marker data object
or a data class
with information to pass on.
@Parcelize\ndata object HomeScreen : Screen\n\n@Parcelize\ndata class AddFavoritesScreen(val externalId: UUID) : Screen\n
These are used by Navigator
s (when called from presenters) or CircuitContent
(when called from UIs) to start a new sub-circuit or nested circuit.
// In a presenter class\nfun showAddFavorites() {\n navigator.goTo(\n AddFavoritesScreen(\n externalId = uuidGenerator.generate()\n )\n )\n}\n
The information passed into a screen can also be used to interact with the data layer. In the example here, we are getting the externalId
from the screen in order to get information back from our repository.
// In a presenter class\nclass AddFavoritesPresenter\n@AssistedInject\nconstructor(\n @Assisted private val screen: AddFavoritesScreen,\n private val favoritesRepository: FavoritesRepository,\n) : Presenter<AddFavoritesScreen.State> {\n @Composable\n override fun present() : AddFavoritesScreen.State {\n val favorite = favoritesRepository.getFavorite(screen.externalId)\n // ...\n }\n}\n
Screens are also used to look up those corresponding components in Circuit
.
val presenter: Presenter<*>? = circuit.presenter(addFavoritesScreen, navigator)\nval ui: Ui<*>? = circuit.ui(addFavoritesScreen)\n
Nomenclature
Semantically, in this example we would call all of these components together the \u201cAddFavorites Screen\u201d.
"},{"location":"setup/","title":"Setting up Circuit","text":"Setting up Circuit is a breeze! Just add the following to your build:
"},{"location":"setup/#installation","title":"Installation","text":"The simplest way to get up and running is with the circuit-foundation
dependency, which includes all the core Circuit artifacts.
dependencies {\n implementation(\"com.slack.circuit:circuit-foundation:<version>\")\n}\n
"},{"location":"setup/#setup","title":"Setup","text":"Create a Circuit
instance. This controls all your common configuration, Presenter/Ui factories, etc.
val circuit = Circuit.Builder()\n .addUiFactory(AddFavoritesUiFactory())\n .addPresenterFactory(AddFavoritesPresenterFactory())\n .build()\n
This configuration can be rebuilt via newBuilder()
and usually would live in your program\u2019s DI graph.
Once you have a configuration ready, the simplest way to get going with Circuit is via CircuitCompositionLocals
. This automatically exposes the config to all child Circuit composables and allows you to get off the ground quickly with CircuitContent
, NavigableCircuitContent
, etc.
CircuitCompositionLocals(circuit) {\n CircuitContent(AddFavoritesScreen())\n}\n
See the docs for CircuitContent
and NavigableCircuitContent
for more information.
Circuit is split into a few different artifacts to allow for more granular control over your dependencies. The following table shows the available artifacts and their purpose:
Artifact ID Dependenciescircuit-backstack
Circuit\u2019s backstack implementation. circuit-runtime
Common runtime components like Screen
, Navigator
, etc. circuit-runtime-presenter
The Presenter
API, depends on circuit-runtime
. circuit-runtime-ui
The Ui
API, depends on circuit-runtime
. circuit-foundation
The Circuit foundational APIs like Circuit
, CircuitContent
, etc. Depends on the first three. circuit-test
First-party test APIs for testing navigation, state emissions, and event sinks. circuit-overlay
Optional Overlay
APIs. circuit-retained
Optional rememberRetained()
APIs."},{"location":"setup/#platform-support","title":"Platform Support","text":"Circuit is a multiplatform library, but not all features are available on all platforms. The following table shows which features are available on which platforms:
Backstack
\u2705 \u2705 \u2705 \u2705 CircuitContent
\u2705 \u2705 \u2705 \u2705 ContentWithOverlays
\u2705 \u2705 \u2705 \u2705 NavigableCircuitContent
\u2705 \u2705 \u2705 \u2705 Navigator
\u2705 \u2705 \u2705 \u2705 SaveableBackstack
\u2705 \u2705 \u2705 \u2705 Saveable is a no-op on non-android. rememberCircuitNavigator
\u2705 \u2705 \u2705 \u2705 rememberRetained
\u2705 \u2705 \u2705 \u2705 TestEventSink
\u2705 \u2705 \u2705 \u2705 On JS you must use asEventSinkFunction()
."},{"location":"states-and-events/","title":"States and Events","text":"The core state and event interfaces in Circuit are CircuitUiState
and CircuitUiEvent
. All state and event types should implement/extend these marker interfaces.
Presenters are simple functions that determine and return composable states. UIs are simple functions that render states. Uis can emit events via eventSink
properties in state classes, which presenters then handle. These are the core building blocks!
States should be @Stable
; events should be @Immutable
.
Wait, event callbacks in state types?
Yep! This may feel like a departure from how you\u2019ve written UDF patterns in the past, but we really like it. We tried different patterns before with event Flow
s and having Circuit internals manage these for you, but we found they came with tradeoffs and friction points that we could avoid by just treating event emissions as another aspect of state. The end result is a tidier structure of state + event flows.
Flow
for events, which comes with caveats in compose (wrapping operators in remember
calls, pipelining nested event flows, etc)Click
may not make sense for Loading
states).Channel
and multicasting event streams.Flow
).Note
Currently, while functions are treated as implicitly Stable
by the compose compiler, they\u2019re not skippable when they\u2019re non-composable Unit-returning lambdas with equal-but-unstable captures. This may change though, and would be another free benefit for this case.
A longer-form writeup can be found in this PR.
"},{"location":"testing/","title":"Testing","text":"Circuit is designed to make testing as easy as possible. Its core components are not mockable nor do they need to be mocked. Fakes are provided where needed, everything else can be used directly.
Circuit will have a test artifact containing APIs to aid testing both presenters and composable UIs:
Presenter.test()
- an extension function that bridges the Compose and coroutines world. Use of this function is recommended for testing presenter state emissions and incoming UI events. Under the hood it leverages Molecule and Turbine.FakeNavigator
- a test fake implementing the Navigator
interface. Use of this object is recommended when testing screen navigation (ie. goTo, pop/back). This acts as a real navigator and exposes recorded information for testing purposes.TestEventSink
- a generic test fake for recording and asserting event emissions through an event sink function.Test helpers are available via the circuit-test
artifact.
testImplementation(\"com.slack.circuit:circuit-test:<version>\")\n
For Gradle JVM projects, you can use Gradle test fixtures syntax on the core circuit artifact.
testImplementation(testFixtures(\"com.slack.circuit:circuit:<version>\"))\n
"},{"location":"testing/#example","title":"Example","text":"Testing a Circuit Presenter and UI is a breeze! Consider the following example:
data class Favorite(id: Long, ...)\n\n@Parcelable\ndata object FavoritesScreen : Screen {\n sealed interface State : CircuitUiState {\n data object Loading : State\n data object NoFavorites : State\n data class Results(\n val list: List<Favorite>,\n val eventSink: (Event) -> Unit\n ) : State\n }\n\n sealed interface Event : CircuitUiEvent {\n data class ClickFavorite(id: Long): Event\n }\n}\n\nclass FavoritesPresenter @Inject constructor(\n navigator: Navigator,\n repo: FavoritesRepository\n) : Presenter<State> {\n @Composable override fun present(): State {\n val favorites by produceState<List<Favorites>?>(null) {\n value = repo.getFavorites()\n }\n\n return when {\n favorites == null -> Loading\n favorites.isEmpty() -> NoFavorites\n else ->\n Results(favorites) { event ->\n when (event) {\n is ClickFavorite -> navigator.goTo(FavoriteScreen(event.id))\n }\n }\n }\n }\n}\n\n@Composable\nfun FavoritesList(state: FavoritesScreen.State) {\n when (state) {\n Loading -> Text(text = stringResource(R.string.loading_favorites))\n NoFavorites -> Text(\n modifier = Modifier.testTag(\"no favorites\"),\n text = stringResource(R.string.no_favorites)\n )\n is Results -> {\n Text(text = \"Your Favorites\")\n LazyColumn {\n items(state.list) { Favorite(it, state.eventSink) }\n }\n }\n }\n}\n\n@Composable\nprivate fun Favorite(favorite: Favorite, eventSink: (FavoritesScreen.Event) -> Unit) {\n Row(\n modifier = Modifier.testTag(\"favorite\"),\n onClick = { eventSink(ClickFavorite(favorite.id)) }\n ) {\n Image(\n drawable = favorite.drawable, \n contentDescription = stringResource(R.string.favorite_image_desc)\n )\n Text(text = favorite.name)\n Text(text = favorite.date)\n }\n}\n
"},{"location":"testing/#presenter-unit-tests","title":"Presenter Unit Tests","text":"Here\u2019s a test to verify presenter emissions using the Presenter.test()
helper. This function acts as a shorthand over Molecule + Turbine to give you a ReceiveTurbine.() -> Unit
lambda.
@Test \nfun `present - emit loading state then list of favorites`() = runTest {\n val favorites = listOf(Favorite(1L, ...))\n\n val repo = TestFavoritesRepository(favorites)\n val presenter = FavoritesPresenter(navigator, repo)\n\n presenter.test {\n assertThat(awaitItem()).isEqualTo(FavoritesScreen.State.Loading)\n val resultsItem = awaitItem() as Results\n assertThat(resultsItem.favorites).isEqualTo(favorites)\n }\n}\n
The same helper can be used when testing how the presenter responds to incoming events:
@Test \nfun `present - navigate to favorite screen`() = runTest {\n val repo = TestFavoritesRepository(Favorite(123L))\n val presenter = FavoritesPresenter(navigator, repo)\n\n presenter.test {\n assertThat(awaitItem()).isEqualTo(FavoritesScreen.State.Loading)\n val resultsItem = awaitItem() as Results\n assertThat(resultsItem.favorites).isEqualTo(favorites)\n val clickFavorite = FavoriteScreen.Event.ClickFavorite(123L)\n\n // simulate user tapping favorite in UI\n resultsItem.eventSink(clickFavorite)\n\n assertThat(navigator.awaitNextScreen()).isEqualTo(FavoriteScreen(clickFavorite.id))\n }\n}\n
"},{"location":"testing/#android-ui-instrumentation-tests","title":"Android UI Instrumentation Tests","text":"UI tests can be driven directly through ComposeTestRule
and use its Espresso-esque API for assertions:
Here is also a good place to use a TestEventSink
and assert expected event emissions from specific UI interactions.
@Test\nfun favoritesList_show_favorites_for_result_state() = runTest {\n val favorites = listOf(Favorite(1L, ...))\n val events = TestEventSink<FavoriteScreen.Event>()\n\n composeTestRule.run {\n setContent { \n // bootstrap the UI in the desired state\n FavoritesList(\n state = FavoriteScreen.State.Results(favorites, events)\n )\n }\n\n onNodeWithTag(\"no favorites\").assertDoesNotExist()\n onNodeWithText(\"Your Favorites\").assertIsDisplayed()\n onAllNodesWithTag(\"favorite\").assertCountEquals(1)\n .get(1)\n .performClick()\n\n events.assertEvent(FavoriteScreen.Event.ClickFavorite(1L))\n }\n}\n
"},{"location":"testing/#snapshot-tests","title":"Snapshot Tests","text":"Because Circuit UIs simply take an input state parameter, snapshot tests via Paparazzi or Roborazzi are a breeze.
This allows allows you to render UI without a physical device or emulator and assert pixel-perfection on the result.
@Test\nfun previewFavorite() {\n paparazzi.snapshot { PreviewFavorite() }\n}\n
These are easy to maintain and review in GitHub.
Another neat idea is we think this will make it easy to stand up compose preview functions for IDE use and reuse them.
// In your main source\n@Preview\n@Composable\ninternal fun PreviewFavorite() {\n Favorite()\n}\n\n// In your unit test\n@Test\nfun previewFavorite() {\n paparazzi.snapshot { PreviewFavorite() }\n}\n
"},{"location":"tutorial/","title":"Tutorial","text":"This tutorial will help you ramp up to Circuit with a simple email app.
Note this assumes some prior experience with Compose. See these resources for more information:
You can do this tutorial in one of two ways:
"},{"location":"tutorial/#1-build-out-of-the-tutorial-sample","title":"1. Build out of thetutorial
sample","text":"Clone the circuit repo and work out of the :samples:tutorial
module. This has all your dependencies set up and ready to go, along with some reusable common code to save you some boilerplate. You can see an implementation of this tutorial there as well.
This can be run on Android or Desktop. - The Desktop entry point is main.kt
. To run the main function, you can run ./gradlew :samples:tutorial:run
. - The Android entry point is MainActivity
. Run ./gradlew :samples:tutorial:installDebug
to install it on a device or emulator.
First, set up Compose in your project. See the following guides for more information:
Next, add the circuit-foundation
dependency. This includes all the core Circuit artifacts.
dependencies {\n implementation(\"com.slack.circuit:circuit-foundation:<version>\")\n}\n
See setup docs for more information.
"},{"location":"tutorial/#create-a-screen","title":"Create aScreen
","text":"The primary entry points in Circuit are Screen
s (docs). These are the navigational building blocks of your app. A Screen
is a simple data class or data object that represents a unique location in your app. For example, a Screen
could represent an inbox list, an email detail, or a settings screen.
Let\u2019s start with a simple Screen
that represents an inbox list:
@Parcelize\ndata object InboxScreen : Screen\n
data object InboxScreen : Screen\n
Tip
Screen
is Parcelable
on Android. You should use the Parcelize plugin to annotate your screens with @Parcelize
.
Next, let\u2019s define some state for our InboxScreen
. Circuit uses unidirectional data flow (UDF) to ensure strong separation between presentation logic and UI. States should be stable or immutable, and directly renderable by your UIs. As such, you should design them to be as simple as possible.
Conventionally, this is written as a nested State
class inside your Screen
and must extend CircuitUiState
(docs).
data object InboxScreen : Screen {\n data class State(\n val emails: List<Email>\n ) : CircuitUiState\n}\n
Email.kt@Immutable\ndata class Email(\n val id: String,\n val subject: String,\n val body: String,\n val sender: String,\n val timestamp: String,\n val recipients: List<String>,\n)\n
See the states and events guide for more information.
"},{"location":"tutorial/#create-your-ui","title":"Create your UI","text":"InboxNext, let\u2019s define a Ui
for our InboxScreen
. A Ui
is a simple composable function that takes State
and Modifier
parameters.
It\u2019s responsible for rendering the state. You should write this like a standard composable. In this case, we\u2019ll use a LazyColumn
to render a list of emails.
@Composable\nfun Inbox(state: InboxScreen.State, modifier: Modifier = Modifier) {\n Scaffold(modifier = modifier, topBar = { TopAppBar(title = { Text(\"Inbox\") }) }) { innerPadding ->\n LazyColumn(modifier = Modifier.padding(innerPadding)) {\n items(state.emails) { email ->\n EmailItem(email)\n }\n }\n }\n}\n\n// Write one or use EmailItem from ui.kt\n@Composable\nprivate fun EmailItem(email: Email, modifier: Modifier = Modifier) {\n // ...\n}\n
For more complex UIs with dependencies, you can create a class that implements the Ui
interface (docs). This is rarely necessary though, and we won\u2019t use this in the tutorial.
class InboxUi(...) : Ui<InboxScreen.State> {\n @Composable\n override fun Content(state: InboxScreen.State, modifier: Modifier) {\n LazyColumn(modifier = modifier) {\n items(state.emails) { email ->\n EmailItem(email)\n }\n }\n }\n}\n
"},{"location":"tutorial/#implement-your-presenter","title":"Implement your presenter","text":"Next, let\u2019s define a Presenter
(docs) for our InboxScreen
. Circuit presenters are responsible for computing and emitting state.
class InboxPresenter : Presenter<InboxScreen.State> {\n @Composable\n override fun present(): InboxScreen.State {\n return InboxScreen.State(\n emails = listOf(\n Email(\n id = \"1\",\n subject = \"Meeting re-sched!\",\n body = \"Hey, I'm going to be out of the office tomorrow. Can we reschedule?\",\n sender = \"Ali Connors\",\n timestamp = \"3:00 PM\",\n recipients = listOf(\"all@example.com\"),\n ),\n // ... more emails\n )\n )\n }\n}\n
This is a trivial implementation that returns a static list of emails. In a real app, you\u2019d likely fetch this data from a repository or other data source. In our tutorial code in the repo, we\u2019ve added a simple EmailRepository
that you can use to fetch emails. It exposes a suspending getEmails()
function that returns a list of emails.
This is also a good opportunity to see where using compose in our presentation logic shines, as we can use Compose\u2019s advanced state management to make our presenter logic more expressive and easy to understand.
InboxScreen.ktclass InboxPresenter(private val emailRepository: EmailRepository) : Presenter<InboxScreen.State> {\n @Composable\n override fun present(): InboxScreen.State {\n val emails by produceState<List<Email>>(initialValue = emptyList()) {\n value = emailRepository.getEmails()\n }\n // Or a flow!\n // val emails by emailRepository.getEmailsFlow().collectAsState(initial = emptyList())\n return InboxScreen.State(emails)\n }\n}\n
Analogous to Ui
, you can also define simple/dependency-less presenters as just a top-level function.
@Composable\nfun InboxPresenter(): InboxScreen.State {\n val emails = ...\n return InboxScreen.State(emails)\n}\n
Tip
Generally, Circuit presenters are implemented as classes and Circuit UIs are implemented as top-level functions. You can mix and match as needed for a given use case. Under the hood, Circuit will wrap all top-level functions into a class for you.
"},{"location":"tutorial/#wiring-it-up","title":"Wiring it up","text":"Now that we have a Screen
, State
, Ui
, and Presenter
, let\u2019s wire them up together. Circuit accomplishes this with the Circuit
class (docs), which is responsible for connecting screens to their corresponding presenters and UIs. These are created with a simple builder pattern.
val emailRepository = EmailRepository()\nval circuit: Circuit =\n Circuit.Builder()\n .addPresenter<InboxScreen, InboxScreen.State>(InboxPresenter(emailRepository))\n .addUi<InboxScreen, InboxScreen.State> { state, modifier -> Inbox(state, modifier) }\n .build()\n
This instance should usually live on your application\u2019s DI graph.
Note
This is a simple example that uses the addPresenter
and addUi
functions. In a real app, you\u2019d likely use a Presenter.Factory
and Ui.Factory
to create your presenters and UIs dynamically.
Once you have this instance, you can plug it into CircuitCompositionLocals
(docs) and be on your way. This is usually a one-time setup in your application at its primary entry point.
class MainActivity {\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n val circuit = Circuit.Builder()\n // ...\n .build()\n\n setContent {\n CircuitCompositionLocals(circuit) {\n // ...\n }\n }\n }\n}\n
main.ktfun main() {\n val circuit = Circuit.Builder()\n // ...\n .build()\n\n application {\n Window(title = \"Inbox\", onCloseRequest = ::exitApplication) {\n CircuitCompositionLocals(circuit) {\n // ...\n }\n }\n }\n}\n
main.ktfun main() {\n val circuit = Circuit.Builder()\n // ...\n .build()\n\n onWasmReady {\n Window(\"Inbox\") {\n CircuitCompositionLocals(circuit) {\n // ...\n }\n }\n }\n}\n
"},{"location":"tutorial/#circuitcontent","title":"CircuitContent
","text":"CircuitContent
(docs) is a simple composable that takes a Screen
and renders it.
CircuitCompositionLocals(circuit) {\n CircuitContent(InboxScreen)\n}\n
Under the hood, this instantiates the corresponding Presenter
and Ui
from the local Circuit
instance and connects them together. All you need to do is pass in the Screen
you want to render!
This is the most basic way to render a Screen
. These can be top-level UIs or nested within other UIs. You can even have multiple CircuitContent
instances in the same composition.
An app architecture isn\u2019t complete without navigation. Circuit provides a simple navigation API that\u2019s focused around a simple BackStack
(docs) that is navigated via a Navigator
interface (docs). In most cases, you can use the built-in SaveableBackStack
implementation (docs), which is saved and restored in accordance with whatever the platform\u2019s rememberSaveable
implementation is.
val backStack = rememberSaveableBackStack(root = InboxScreen)\nval navigator = rememberCircuitNavigator(backStack) {\n // Do something when the root screen is popped, usually exiting the app\n}\n
Once you have these two components created, you can pass them to an advanced version of CircuitContent
that supports navigation called NavigableCircuitContent
(docs).
NavigableCircuitContent(navigator = navigator, backstack = backStack)\n
This composable will automatically manage the backstack and navigation for you, essentially rendering the \u201ctop\u201d of the back stack as your navigator navigates it. This also handles transitions between screens (NavDecoration
) and fallback behavior with Circuit.Builder.onUnavailableRoute
(docs).
Like with Circuit
, this is usually a one-time setup in your application at its primary entry point.
val backStack = rememberSaveableBackStack(root = InboxScreen)\nval navigator = rememberCircuitNavigator(backStack) {\n // Do something when the root screen is popped, usually exiting the app\n}\nCircuitCompositionLocals(circuit) {\n NavigableCircuitContent(navigator = navigator, backstack = backStack)\n}\n
"},{"location":"tutorial/#add-a-detail-screen","title":"Add a detail screen","text":"Detail Now that we have navigation set up, let\u2019s add a detail screen to our app to navigate to.
This screen will show the content of a specific email from the inbox, and in a real app would also show content like the chain history.
First, let\u2019s define a DetailScreen
and state.
@Parcelize\ndata class DetailScreen(val emailId: String) : Screen {\n data class State(val email: Email) : CircuitUiState\n}\n
DetailScreen.ktdata class DetailScreen(val emailId: String) : Screen {\n data class State(val email: Email) : CircuitUiState\n}\n
Notice that this time we use a data class
instead of a data object
. This is because we want to be able to pass in an emailId
to the screen. We\u2019ll use this to fetch the email from our data layer.
Warning
You should keep Screen
parameters as simple as possible and derive any additional data you need from your data layer instead.
Next, let\u2019s define a Presenter and UI for this screen.
PresenterUI DetailScreen.ktclass DetailPresenter(\n private val screen: DetailScreen,\n private val emailRepository: EmailRepository\n) : Presenter<DetailScreen.State> {\n @Composable\n override fun present(): DetailScreen.State {\n val email = emailRepository.getEmail(screen.emailId)\n return DetailScreen.State(email)\n }\n}\n
DetailScreen.kt@Composable\nfun EmailDetail(state: DetailScreen.State, modifier: Modifier = Modifier) {\n // ...\n // Write one or use EmailDetailContent from ui.kt\n}\n
Note that we\u2019re injecting the DetailScreen
into our Presenter
so we can get the email ID. This is where Circuit\u2019s factory pattern comes into play. Let\u2019s define a factory for our DetailPresenter
.
class DetailPresenter(...) : Presenter<DetailScreen.State> {\n // ...\n class Factory(private val emailRepository: EmailRepository) : Presenter.Factory {\n override fun create(screen: Screen, navigator: Navigator, context: CircuitContext): Presenter<*>? {\n return when (screen) {\n is DetailScreen -> return DetailPresenter(screen, emailRepository)\n else -> null\n }\n }\n }\n}\n
Here we have access to the screen and dynamically create the presenter we need. It can then pass the screen on to the presenter.
We can then wire these detail components to our Circuit
instance too.
val circuit: Circuit =\n Circuit.Builder()\n // ...\n .addPresenterFactory(DetailPresenter.Factory(emailRepository))\n .addUi<DetailScreen, DetailScreen.State> { state, modifier -> EmailDetail(state, modifier) }\n .build()\n
"},{"location":"tutorial/#navigate-to-the-detail-screen","title":"Navigate to the detail screen","text":"Now that we have a detail screen, let\u2019s navigate to it from our inbox list. As you can see in our presenter factory above, Circuit also offers access to a Navigator
in this create()
call that factories can then pass on to their created presenters.
Let\u2019s add a Navigator
property to our presenter and create a factory for our inbox screen now.
class InboxPresenter(\n private val navigator: Navigator,\n private val emailRepository: EmailRepository\n) : Presenter<InboxScreen.State> {\n // ...\n class Factory(private val emailRepository: EmailRepository) : Presenter.Factory {\n override fun create(screen: Screen, navigator: Navigator, context: CircuitContext): Presenter<*>? {\n return when (screen) {\n InboxScreen -> return InboxPresenter(navigator, emailRepository)\n else -> null\n }\n }\n }\n}\n
val circuit: Circuit =\n Circuit.Builder()\n .addPresenterFactory(InboxPresenter.Factory(emailRepository))\n .addUi<InboxScreen, InboxScreen.State> { state, modifier -> Inbox(state, modifier) }\n .addPresenterFactory(DetailPresenter.Factory(emailRepository))\n .addUi<DetailScreen, DetailScreen.State> { state, modifier -> EmailDetail(state, modifier) }\n .build()\n
Now that we have a Navigator
in our inbox presenter, we can use it to navigate to the detail screen. First, we need to explore how events work in Circuit.
So far, we\u2019ve covered state. State is produced by the presenter and consumed by the UI. That\u2019s only half of the UDF picture though! Events are the inverse: they\u2019re produced by the UI and consumed by the presenter. Events are how you can trigger actions in your app, such as navigation. This completes the circuit.
Events in Circuit are a little unconventional in that Circuit doesn\u2019t provide structured APIs for pipelining events from the UI to presenters. Instead, we use an event sink property pattern, where states contain a trailing eventSink
function that receives events emitted from the UI.
This provides many benefits, see the events guide for more information.
Let\u2019s add an event to our inbox screen for when the user clicks on an email.
Events must implement CircuitUiEvent
(docs) and are usually modeled as a sealed interface
hierarchy, where each subtype is a different event type.
data object InboxScreen : Screen {\n data class State(\n val emails: List<Email>,\n val eventSink: (Event) -> Unit\n ) : CircuitUiState\n sealed class Event : CircuitUiEvent {\n data class EmailClicked(val emailId: String) : Event()\n }\n}\n
Now that we have an event, let\u2019s emit it from our UI.
InboxScreen.kt@Composable\nfun Inbox(state: InboxScreen.State, modifier: Modifier = Modifier) {\n Scaffold(modifier = modifier, topBar = { TopAppBar(title = { Text(\"Inbox\") }) }) { innerPadding ->\n LazyColumn(modifier = Modifier.padding(innerPadding)) {\n items(state.emails) { email ->\n EmailItem(\n email = email,\n onClick = { state.eventSink(InboxScreen.Event.EmailClicked(email.id)) },\n )\n }\n }\n }\n}\n\n// Write one or use EmailItem from ui.kt\nprivate fun EmailItem(email: Email, modifier: Modifier = Modifier, onClick: () -> Unit) {\n // ...\n}\n
Finally, let\u2019s handle this event in our presenter.
InboxScreen.ktclass InboxPresenter(\n private val navigator: Navigator,\n private val emailRepository: EmailRepository\n) : Presenter<InboxScreen.State> {\n @Composable\n override fun present(): InboxScreen.State {\n // ...\n return InboxScreen.State(emails) { event ->\n when (event) {\n // Navigate to the detail screen when an email is clicked\n is EmailClicked -> navigator.goTo(DetailScreen(event.emailId))\n }\n }\n }\n}\n
This demonstrates how we can navigate forward in our app and pass data with it. Let\u2019s see how we can navigate back.
"},{"location":"tutorial/#navigating-back","title":"Navigating back","text":"Naturally, navigation can\u2019t be just one way. The opposite of Navigator.goTo()
is Navigator.pop()
, which pops the back stack back to the previous screen. To use this, let\u2019s add a back button to our detail screen and wire it up to a Navigator
.
data class DetailScreen(val emailId: String) : Screen {\n data class State(\n val email: Email,\n val eventSink: (Event) -> Unit\n ) : CircuitUiState\n sealed class Event : CircuitUiEvent {\n data object BackClicked : Event()\n }\n}\n
DetailScreen.kt@Composable\nfun EmailDetail(state: DetailScreen.State, modifier: Modifier = Modifier) {\n Scaffold(\n modifier = modifier,\n topBar = {\n TopAppBar(\n title = { Text(state.email.subject) },\n navigationIcon = {\n IconButton(onClick = { state.eventSink(DetailScreen.Event.BackClicked) }) {\n Icon(Icons.Default.ArrowBack, contentDescription = \"Back\")\n }\n },\n )\n },\n ) { innerPadding ->\n // Remaining detail UI...\n }\n}\n
DetailScreen.ktclass DetailPresenter(\n private val screen: DetailScreen,\n private val navigator: Navigator,\n private val emailRepository: EmailRepository,\n) : Presenter<DetailScreen.State> {\n @Composable\n override fun present(): DetailScreen.State {\n // ...\n return DetailScreen.State(email) { event ->\n when (event) {\n DetailScreen.Event.BackClicked -> navigator.pop()\n }\n }\n }\n // ...\n}\n
On Android, NavigableCircuitContent
automatically hooks into BackHandler to automatically pop on system back presses. On Desktop, it\u2019s recommended to wire the ESC key.
This is just a brief introduction to Circuit. For more information see various docs on the site, samples in the repo, the API reference, and check out other Circuit tools like circuit-retained, CircuitX, factory code gen, overlays, navigation with results, testing, multiplatform, and more.
"},{"location":"ui/","title":"UI","text":"The core Ui interface is simply this:
interface Ui<UiState : CircuitUiState> {\n @Composable fun Content(state: UiState, modifier: Modifier)\n}\n
Like presenters, simple UIs can also skip the class all together for use in other UIs. Core unit of granularity is just the @Composable function. In fact, when implementing these in practice they rarely use dependency injection at all and can normally just be written as top-level composable functions annotated with@CircuitInject
.
@CircuitInject<FavoritesScreen> // Relevant DI wiring is generated\n@Composable\nprivate fun Favorites(state: FavoritesState, modifier: Modifier = Modifier) {\n // ...\n}\n
Writing UIs like this has a number of benefits.
Let\u2019s look a little more closely at the last bullet point about preview functions. With the above example, we can easily stand up previews for all of our different states!
@Preview\n@Composable\nprivate fun PreviewFavorites() = Favorites(FavoritesState(listOf(\"Reeses\", \"Lola\")))\n\n@Preview\n@Composable\nprivate fun PreviewEmptyFavorites() = Favorites(FavoritesState(listOf()))\n
"},{"location":"ui/#static-ui","title":"Static UI","text":"In some cases, a UI may not need a presenter to compute or manage its state. Examples of this include UIs that are stateless or can derive their state from a single static input or an input [Screen]\u2019s properties. In these cases, make your screen implement the StaticScreen
interface. When a StaticScreen
is used, Circuit will internally allow the UI to run on its own and won\u2019t connect it to a presenter if no presenter is provided.
Circuit is used in production at Slack and ready for general use \ud83d\ude80. The API is considered unstable as we continue to iterate on it.
"},{"location":"#overview","title":"Overview","text":"Circuit is a simple, lightweight, and extensible framework for building Kotlin applications that\u2019s Compose from the ground up.
Compose Runtime vs. Compose UI
Compose itself is essentially two libraries \u2013 Compose Compiler and Compose UI. Most folks usually think of Compose UI, but the compiler (and associated runtime) are actually not specific to UI at all and offer powerful state management APIs.
Jake Wharton has an excellent post about this: https://jakewharton.com/a-jetpack-compose-by-any-other-name/
It builds upon core principles we already know like Presenters and UDF, and adds native support in its framework for all the other requirements we set out for above. It\u2019s heavily influenced by Cash App\u2019s Broadway architecture (talked about at Droidcon NYC, also very derived from our conversations with them).
Circuit\u2019s core components are its Presenter
and Ui
interfaces.
Presenter
and a Ui
cannot directly access each other. They can only communicate through state and event emissions.Presenter
and Ui
each have a single composable function.Presenter
and Ui
are both generic types, with generics to define the UiState
types they communicate with.Screen
s. One runs a new Presenter
/Ui
pairing by requesting them with a given Screen
that they understand.Screens
The pairing of a Presenter
and Ui
for a given Screen
key is what we semantically call a \u201cscreen\u201d.
Presenter
+ Ui
pairing would be a \u201ccounter screen\u201d.Circuit\u2019s repo (https://github.com/slackhq/circuit) is being actively developed in the open, which allows us to continue collaborating with external folks too. We have a trivial-but-not-too-trivial sample app that we have been developing in it to serve as a demo for a number of common patterns in Circuit use.
"},{"location":"#counter-example","title":"Counter Example","text":"This is a very simple case of a Counter screen that displays the count and has buttons to increment and decrement.
There\u2019s some glue code missing from this example that\u2019s covered in the Code Gen section later.
@Parcelize\ndata object CounterScreen : Screen {\n data class CounterState(\n val count: Int,\n val eventSink: (CounterEvent) -> Unit,\n ) : CircuitUiState\n sealed interface CounterEvent : CircuitUiEvent {\n data object Increment : CounterEvent\n data object Decrement : CounterEvent\n }\n}\n\n@CircuitInject(CounterScreen::class, AppScope::class)\n@Composable\nfun CounterPresenter(): CounterState {\n var count by rememberSaveable { mutableStateOf(0) }\n\n return CounterState(count) { event ->\n when (event) {\n CounterEvent.Increment -> count++\n CounterEvent.Decrement -> count--\n }\n }\n}\n\n@CircuitInject(CounterScreen::class, AppScope::class)\n@Composable\nfun Counter(state: CounterState) {\n Box(Modifier.fillMaxSize()) {\n Column(Modifier.align(Alignment.Center)) {\n Text(\n modifier = Modifier.align(CenterHorizontally),\n text = \"Count: ${state.count}\",\n style = MaterialTheme.typography.displayLarge\n )\n Spacer(modifier = Modifier.height(16.dp))\n Button(\n modifier = Modifier.align(CenterHorizontally),\n onClick = { state.eventSink(CounterEvent.Increment) }\n ) { Icon(rememberVectorPainter(Icons.Filled.Add), \"Increment\") }\n Button(\n modifier = Modifier.align(CenterHorizontally),\n onClick = { state.eventSink(CounterEvent.Decrement) }\n ) { Icon(rememberVectorPainter(Icons.Filled.Remove), \"Decrement\") }\n }\n }\n}\n
"},{"location":"#license","title":"License","text":"Copyright 2022 Slack Technologies, LLC\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n
"},{"location":"changelog/","title":"Changelog","text":""},{"location":"changelog/#unreleased","title":"Unreleased","text":""},{"location":"changelog/#0222","title":"0.22.2","text":"2024-06-04
pausableState
recomposition loops by avoiding backward snapshot writes.Circuit.presentWithLifecycle
flag to enable/disable automatic pausableState
use. This is enabled by default.1.6.11
.2.8.1
.1.7.0
.Special thanks to @chrisbanes, @adamp, and Chuck Jazdzewski for contributing to this release and helping us find a runtime fix for the pausableState
issue!
2024-05-28
rememberRetained
implicitly requiring LocalContext
where it used to no-op.2.0.0
.2024-05-28
2.0.0
.2.0.0-1.0.21
.1.6.10
.This release is otherwise identical to 0.21.0
, just updated to Kotlin 2.0.
2024-05-28
FakeNavigator
functions to check for the lack of pop/resetRoot events.FakeNavigator
constructor param to add additional screens to the backstack.StaticScreen
interface. When a StaticScreen
is used, Circuit will internally allow the UI to run on its own and won\u2019t connect it to a presenter if no presenter is provided.RecordLifecycle
and LocalRecordLifecycle
composition local, allowing UIs and presenters to observe when they are \u2018active\u2019. Currently, a record is considered \u2018active\u2019 when it is the top record on the back stack.rememberRetainedSaveable
variant that participates in both RetainedStateRegistry
and SaveableStateRegistry
restoration, allowing layered state persistence.rememberRetainedSaveable
entering composition:RetainedStateRegistry
and SaveableStateRegistry
, if availablerememberRetained
that explicitly requires a Saver
parameter.CircuitUiState
when they are not active. Presenters can opt-out of this behavior by implementing NonPausablePresenter
.NavigatorImpl.goTo
no longer navigates if the Screen
is equal to Navigator.peek()
.Presenter.present
is now annotated with @ComposableTarget(\"presenter\")
. This helps prevent use of Compose UI in the presentation logic as the compiler will emit a warning if you do. Note this does not appear in the IDE, so it\u2019s recommended to use allWarningsAsErrors
to fail the build on this event.Navigator.goTo()
calls to the same current screen.Navigator.goTo
now returns a Bool indicating navigation success.GestureNavigationDecoration
impl to commonMain
and rename to CupertinoGestureNavigationDecoration
.1.8
in core libraries.FakeNavigator.assertIsEmpty
and expectNoEvents
(use the specific event type methods instead)Presenter.Factory
as @Stable
.Ui.Factory
as @Stable
.CircuitContext
as @Stable
.EventListener
as @Stable
.EventListener.Factory
as @Stable
.1.9.24
.1.9.24-2.0.20
.1.5.14
.1.17.0
.2.8.0
.1.4.3
.1.8.0
.1.8.1
.1.6.2
.1.6.7
.1.6.7
.1.6.7
.1.6.7
.1.13.1
.1.9.0
.2.51.1
.0.8.4
.Special thanks to @chrisbanes, @alexvanyo, @eboudrant, @edenman, and @JustinBis for contributing to this release!
"},{"location":"changelog/#0200","title":"0.20.0","text":"2024-03-18
RememberObserver
to work with rememberRetained
.Navigator.popRoot()
. extension (#1274)CircuitContent
to keep Ui
and Presenter
consistent. We already did this for presenters, this just makes it consistent for both.ToastEffect
.rememberImpressionNavigator()
not delegating PopResult
.PopResult
to onRootPop()
.canRetainCheck
when saving RetainedStateRegistry
.com.google.guava:listenablefuture
to 1.0
to avoid conflicts with Guava.1.5.10.1
.1.8.0
.1.6.1
.1.6.3
.1.4.1
.2.51
.1.1.0
.0.8.3
.1.9.23
.1.9.23-1.0.19
.Special thanks to @chrisbanes, @aschulz90, and @alexvanyo for contributing to this release!
"},{"location":"changelog/#0191","title":"0.19.1","text":"2024-02-12
This is a small bug fix release focused SaveableBackStack
consistency and FakeNavigator
API improvements.
FakeNavigator.awaitNextScreen()
not suspending.FakeNavigator.resetRoot()
not returning the actual popped screens.Navigator.peekBackStack()
and Navigator.resetRoot()
return ImmutableList
.BackStack.popUntil()
return the ImmutableList
of the popped records.FakeNavigator.peekBackStack()
return the ImmutableList
of the popped records.FakeNavigator
. This should offer much more information about the events.BackStack
instance in FakeNavigator
+ allow for specifying a user-provided instance.FakeNavigator
unless using a custom BackStack
.goTo
event.rememberSaveableBackStack()
.Navigator()
factory function.2024-02-09
"},{"location":"changelog/#navigation-with-results","title":"Navigation with results","text":"This release introduces support for inter-screen navigation results. This is useful for scenarios where you want to pass data back to the previous screen after a navigation event, such as when a user selects an item from a list and you want to pass the selected item back to the previous screen.
var photoUrl by remember { mutableStateOf<String?>(null) }\nval takePhotoNavigator = rememberAnsweringNavigator<TakePhotoScreen.Result>(navigator) { result ->\n photoUrl = result.url\n}\n\n// Elsewhere\ntakePhotoNavigator.goTo(TakePhotoScreen)\n\n// In TakePhotoScreen.kt\ndata object TakePhotoScreen : Screen {\n @Parcelize\n data class Result(val url: String) : PopResult\n}\n\nclass TakePhotoPresenter {\n @Composable fun present(): State {\n // ...\n navigator.pop(result = TakePhotoScreen.Result(newFilters))\n }\n}\n
See the new section in the navigation docs for more details, as well as updates to the Overlays docs that help explain when to use an Overlay
vs navigating to a Screen
with a result.
This release introduces support for saving/restoring navigation state on root resets (aka multi back stack). This is useful for scenarios where you want to reset the back stack to a new root but still want to retain the previous back stack\u2019s state, such as an app UI that has a persistent bottom navigation bar with different back stacks for each tab.
This works by adding two new optional saveState
and restoreState
parameters to Navigator.resetRoot()
.
navigator.resetRoot(HomeNavTab1, saveState = true, restoreState = true)\n// User navigates to a details screen\nnavigator.push(EntityDetails(id = foo))\n// Later, user clicks on a bottom navigation item\nnavigator.resetRoot(HomeNavTab2, saveState = true, restoreState = true)\n// Later, user switches back to the first navigation item\nnavigator.resetRoot(HomeNavTab1, saveState = true, restoreState = true)\n// The existing back stack is restored, and EntityDetails(id = foo) will be top of\n// the back stack\n
There are times when saving and restoring the back stack may not be appropriate, so use this feature only when it makes sense. A common example where it probably does not make sense is launching screens which define a UX flow which has a defined completion, such as onboarding.
"},{"location":"changelog/#new-tutorial","title":"New Tutorial!","text":"On top of Circuit\u2019s existing docs, we\u2019ve added a new tutorial to help you get started with Circuit. It\u2019s a step-by-step guide that walks you through building a simple inbox app using Circuit, intended to serve as a sort of small code lab that one could do in 1-2 hours. Check it out here.
"},{"location":"changelog/#overlay-improvements","title":"Overlay Improvements","text":"AlertDialogOverlay
, BasicAlertDialogOverlay
, and BasicDialogOverlay
to circuitx-overlay
.OverlayEffect
to circuit-overlay
. This offers a simple composable effect to show an overlay and await a result. OverlayEffect(state) { host ->\n val result = host.show(AlertDialogOverlay(...))\n // Do something with the result\n}\n
OverlayState
and LocalOverlayState
to circuit-overlay
. This allows you to check the current overlay state (UNAVAILABLE
, HIDDEN
, or SHOWING
).OverlayHost
as @ReadOnlyOverlayApi
to indicate that it\u2019s not intended for direct implementation by consumers.Overlay
as @Stable
.NavEvent.screen
public.Navigator.popUntil
to be exclusive.Navigator.peek()
to peek the top screen of the back stack.Navigator.peekBackStack()
to peek the top screen of the back stack.backStack
.BackStack.Record
as @Stable
.onRootPop
of the Android rememberCircuitNavigator
.2.7.0
.1.5.12
.1.6.1
.2024.02.00
.1.5.9
.0.23.2
.2.4.9
.1.16.0
.1.9.22-1.0.17
.Special thanks to @milis92, @ChrisBanes, and @vulpeszerda for contributing to this release!
"},{"location":"changelog/#0182","title":"0.18.2","text":"2024-01-05
Record
s\u2019 ViewModelStores
. This fully fixes #1065.1.3.2
.1.5.7.1
.Special thanks to @dandc87 for contributing to this release!
"},{"location":"changelog/#0181","title":"0.18.1","text":"2024-01-01
ProvidedValues
lifetime. See #1065 for more details.GestureNavDecoration
dropping saveable/retained state on back gestures. See #1089 for more details.Special thanks to @ChrisBanes and @dandc87 for contributing to this release!
"},{"location":"changelog/#0180","title":"0.18.0","text":"2023-12-29
AnimatedOverlay
.ModalBottomSheet
appearance in BottomSheetOverlay
.1.9.22
.1.9.22-1.0.16
.2.50
.0.3.7
.1.8.2
.Special thanks to @ChrisBanes, @chriswiesner, and @BryanStern for contributing to this release!
"},{"location":"changelog/#0171","title":"0.17.1","text":"2023-12-05
SaveableStateRegistryBackStackRecordLocalProvider
to be supported across all currently supported platforms.LocalBackStackRecordLocalProviders
always returning a new composition local.androidx.compose.compiler:compiler
to 1.5.5
1.15.3
2.49
Special thanks to @alexvanyo for contributing to this release.
"},{"location":"changelog/#0170","title":"0.17.0","text":"2023-11-28
"},{"location":"changelog/#new-circuitx-effects-artifact","title":"New: circuitx-effects artifact","text":"The circuitx-effects artifact provides some effects for use with logging/analytics. These effects are typically used in Circuit presenters for tracking impressions
and will run only once until forgotten based on the current circuit-retained strategy.
dependencies {\n implementation(\"com.slack.circuit:circuitx-effects:<version>\")\n}\n
Docs: https://slackhq.github.io/circuit/circuitx/#effects
"},{"location":"changelog/#new-add-codegen-mode-to-support-both-anvil-and-hilt","title":"New: Add codegen mode to support both Anvil and Hilt","text":"Circuit\u2019s code gen artifact now supports generating for Hilt projects. See the docs for usage instructions: https://slackhq.github.io/circuit/code-gen/
"},{"location":"changelog/#misc_1","title":"Misc","text":"CircuitContent
internals like rememberPresenter()
, rememberUi
, etc for reuse.CircuitContent()
overload that accepts a pre-constructed presenter/ui parameters public to allow for more control over content.0.8.2
.1.15.1
.1.5.11
.1.9.21
.1.9.21-1.0.15
.1.5.4
.1.3.1
.Special thanks to @jamiesanson, @frett, and @bryanstern for contributing to this release!
"},{"location":"changelog/#0161","title":"0.16.1","text":"2023-11-09
1.9.20-1.0.14
.2023-11-01
circut-retained
is now enabled automatically in CircuitCompositionLocals
by default, we still allowing overriding it with no-op implementation.1.9.20
.1.5.2
.agp
to 8.1.2
.androidx.activity
to 1.8.0
.benchmark
to 1.2.0
.coil
to 2.5.0
.compose.material3
to 1.1.2
.compose.material
to 1.5.4
.compose.runtime
to 1.5.4
.compose.ui
to 1.5.4
.roborazzi
to 1.6.0
.2023-09-20
"},{"location":"changelog/#new-allow-retained-state-to-be-retained-whilst-uis-and-presenters-are-on-the-back-stack","title":"New: Allow retained state to be retained whilst UIs and Presenters are on the back stack.","text":"Originally, circuit-retained
was implemented as a solution for preserving arbitrary data across configuration changes on Android. With this change it now also acts as a solution for retaining state across the back stack, meaning that traversing the backstack no longer causes restored contents to re-run through their empty states anymore.
To support this, each back stack entry now has its own RetainedStateRegistry
instance.
Note that circuit-retained
is still optional for now, but we are considering making it part of CircuitCompositionLocals
in the future. Please let us know your thoughts in this issue: https://github.com/slackhq/circuit/issues/891.
Full details + demos can be found in https://github.com/slackhq/circuit/pull/888. Big thank you to @chrisbanes for the implementation!
"},{"location":"changelog/#other-changes","title":"Other changes","text":"collectAsRetainedState
utility function, analogous to collectAsState
but will retain the previous value across configuration changes and back stack entries.rememberRetained
with a port of the analogous optimization in rememberSaveable
. See #850.Presenter
and Ui
interfaces are now annotated as @Stable
.GestureNavigationDecoration
function parameter order.BackHandler
on iOS now has the proper file name.presenter.present()
in CircuitContent
on the Screen
rather than the presenter
itself, which fixes a severe issue that prevented currentCompositeKeyHash
from working correctly on rememberRetained
and rememberSaveable
uses.1.5.2
.1.5.1
.androidx.compose.animation
to 1.5.1
.androidx.compose.foundation
to 1.5.1
.androidx.compose.runtime
to 1.5.1
.androidx.compose.material
to 1.5.1
.androidx.lifecycle
to 2.6.2
.androidx.annotation
to 1.7.0
.2023-09-03
GestureNavigationDecoration
to CircuitX
courtesy of @chrisbanes.This is a new NavDecoration
that allows for gesture-based navigation, such as predictive back in Android 14 or drag gestures in iOS. See the docs for more details.
NavigableCircuitContent(\n navigator = navigator,\n backstack = backstack,\n decoration = GestureNavigationDecoration(\n // Pop the back stack once the user has gone 'back'\n navigator::pop\n )\n)\n
Special thanks to @chrisbanes and @alexvanyo for contributing to this release!
"},{"location":"changelog/#0140","title":"0.14.0","text":"2023-08-30
Overlay
types or Android navigation interop. See the docs for more details.Screen
to its own artifact. This is now under the com.slack.circuit.runtime.screen.Screen
name.Screen
directly in the BackStack
in place of route
.SaveableBackStack
in NavigableCircuitContent
, now any BackStack
impl is supported.CanRetainChecker
more customizable in circuit-retained
.DecoratedContent
, allowing more complex handling of back gestures (predictive back in android, drag gestures in iOS, etc).buildCircuitContentProviders()
in NavigableCircuitContent
, which enables movableContentOf
to work since it\u2019s reusing the same instance for records across changes.Modifier
for DecoratedContent
.circuit-test
artifact.kotlinx.collections.immutable
to core APIs.1.5.0
.1.5.3
.1.5.0
.1.5.0
.1.5.0
.1.5.0
.0.8.1
.1.2.0
.1.9.10
.1.9.10-1.0.13
.Thanks to @chrisbanes and @ashdavies for contributing to this release!
"},{"location":"changelog/#0130-beta01","title":"0.13.0-beta01","text":"2023-08-17
Overlay
types or Android navigation interop. See the docs for more details.circuit-test
artifact.1.5.0-beta02
.1.5.0
.1.5.0
.1.5.0
.1.5.0
.1.2.0
.1.9.0-1.0.13
.Note this release is a beta release due to the dependency on CM 1.5.0-beta02
.
2023-08-01
2.4.7
.2023-07-28
CircuitConfig
-> Circuit
. There is a source-compatible typealias for CircuitConfig
left with a deprecation replacement to ease migration.CircuitContext.config
-> CircuitContext.circuit
. The previous CircuitContext.config
function is left with a deprecation replacement to ease migration.TestEventSink
helper for testing event emissions in UI tests.1.9.0
.1.9.0-1.0.12
.1.4.3
.1.7.3
.1.5.1
(androidx) and 1.5.0
(compose-multiplatform).0.8.0
.2023-07-20
EventListener.start()
callback.1.0.0
.Thanks to @bryanstern for contributing to this release!
"},{"location":"changelog/#0101","title":"0.10.1","text":"2023-07-09
CircuitContent
overload with Navigator
public.Presenter
and Ui
in CircuitContent
.RememberRetained
.Special thanks to @chrisbanes and @bryanstern for contributing to this release!
"},{"location":"changelog/#0100","title":"0.10.0","text":"2023-06-30
RetainedStateRegistry
instances.0.11.0
.1.4.8
.1.4.1
.1.7.2
.1.0.0
.1.8.22
.Special thanks to @bryanstern, @saket, and @chrisbanes for contributing to this release!
"},{"location":"changelog/#091","title":"0.9.1","text":"2023-06-02
NavEvent
subtypes to public API.com.benasher44:uuid
to 0.7.1
.2.4.6
.2023-05-26
"},{"location":"changelog/#preliminary-support-for-ios-targets","title":"Preliminary support for iOS targets","text":"Following the announcement of Compose for iOS alpha, this release adds ios()
and iosSimulatorArm64()
targets for the Circuit core artifacts. Note that this support doesn\u2019t come with any extra APIs yet for iOS, just basic target support only. We\u2019re not super sure what direction we want to take with iOS, but encourage others to try it out and let us know what patterns you like. We have updated the Counter sample to include an iOS app target as well, using Circuit for the presentation layer only and SwiftUI for the UI.
Note that circuit-codegen and circuit-codegen-annotations don\u2019t support these yet, as Anvil and Dagger only support JVM targets.
More details can be found in the PR: https://github.com/slackhq/circuit/pull/583
"},{"location":"changelog/#misc_2","title":"Misc","text":"Note that we unintentionally used an experimental animation API for NavigatorDefaults.DefaultDecotration
, which may cause R8 issues if you use a newer, experimental version of Compose animation. To avoid issues, copy the animation code and use your own copy compiled against the newest animation APIs. We\u2019ll fix this after Compose 1.5.0 is released.
androidx.activity -> 1.7.2\ncompose -> 1.4.3\ncompose-compiler -> 1.4.7\ncoroutines -> 1.7.1\nkotlin -> 1.8.21\nkotlinpoet -> 1.13.2\nturbine -> 0.13.0\n
"},{"location":"changelog/#080","title":"0.8.0","text":"2023-04-06
"},{"location":"changelog/#core-split-up-core-artifacts","title":"[Core] Split up core artifacts.","text":"circuit-runtime
: common runtime components like Screen
, Navigator
, etc.circuit-runtime-presenter
: the Presenter
API, depends on circuit-runtime
.circuit-runtime-ui
: the Ui
API, depends on circuit-runtime
.circuit-foundation
: the circuit foundational APIs like CircuitConfig
, CircuitContent
, etc. Depends on the first three.The goal in this is to allow more granular dependencies and easier building against subsets of the API. For example, this would allow a presenter implementation to easily live in a standalone module that doesn\u2019t depend on any UI dependencies. Vice versa for UI implementations.
Where we think this could really shine is in multiplatform projects where Circuit\u2019s UI APIs may be more or less abstracted away in service of using native UI, like in iOS.
"},{"location":"changelog/#circuit-runtime-artifact","title":"circuit-runtime
artifact","text":"Before After com.slack.circuit.CircuitContext com.slack.circuit.runtime.CircuitContext com.slack.circuit.CircuitUiState com.slack.circuit.runtime.CircuitUiState com.slack.circuit.CircuitUiEvent com.slack.circuit.runtime.CircuitUiEvent com.slack.circuit.Navigator com.slack.circuit.runtime.Navigator com.slack.circuit.Screen com.slack.circuit.runtime.Screen"},{"location":"changelog/#circuit-runtime-presenter-artifact","title":"circuit-runtime-presenter
artifact","text":"Before After com.slack.circuit.Presenter com.slack.circuit.runtime.presenter.Presenter"},{"location":"changelog/#circuit-runtime-ui-artifact","title":"circuit-runtime-ui
artifact","text":"Before After com.slack.circuit.Ui com.slack.circuit.runtime.presenter.Ui"},{"location":"changelog/#circuit-foundation-artifact","title":"circuit-foundation
artifact","text":"Before After com.slack.circuit.CircuitCompositionLocals com.slack.circuit.foundation.CircuitCompositionLocals com.slack.circuit.CircuitConfig com.slack.circuit.foundation.CircuitConfig com.slack.circuit.CircuitContent com.slack.circuit.foundation.CircuitContent com.slack.circuit.EventListener com.slack.circuit.foundation.EventListener com.slack.circuit.NavEvent com.slack.circuit.foundation.NavEvent com.slack.circuit.onNavEvent com.slack.circuit.foundation.onNavEvent com.slack.circuit.NavigableCircuitContent com.slack.circuit.foundation.NavigableCircuitContent com.slack.circuit.NavigatorDefaults com.slack.circuit.foundation.NavigatorDefaults com.slack.circuit.rememberCircuitNavigator com.slack.circuit.foundation.rememberCircuitNavigator com.slack.circuit.push com.slack.circuit.foundation.push com.slack.circuit.screen com.slack.circuit.foundation.screen"},{"location":"changelog/#more-highlights","title":"More Highlights","text":"NavigableCircuitContent
and just use common one. Back handling still runs through BackHandler
, but is now configured in rememberCircuitNavigator
.defaultNavDecoration
to CircuitConfig
to allow for customizing the default NavDecoration
used in NavigableCircuitContent
.CircuitUiState
as @Stable
instead of @Immutable
.:samples:tacos
order builder sample to demonstrate complex state management.NavigableCircuitContent
example in the desktop counter.1.4.1
.1.4.4
.1.7.0
.0.7.1
.2023-02-10
NavigableCircuitContent
! Special thanks to @ashdavies for contributions to make this possible.circuit-retained
minSdk is now 21 again. We accidentally bumped it to 28 when merging in its instrumentation tests.circuit-core
artifact.circuit-retained
is now covered in embedded baseline profiles.2.45
.1.8.10-1.0.9
.1.4.2
.1.8.10
.2023-02-02
Happy groundhog day!
Ui.Content()
now contains a Modifier
parameter.This allows you to pass modifiers on to UIs directly.
public interface Ui<UiState : CircuitUiState> {\n- @Composable public fun Content(state: UiState)\n+ @Composable public fun Content(state: UiState, modifier: Modifier)\n }\n
Navigator.resetRoot(Screen)
function to reset the backstack root with a new root screen. There is a corresponding awaitResetRoot()
function added to FakeNavigator
.EventListener.start
callback function.Modifier
in the API).CircuitContext.putTag
generics.EventListener.onState
\u2019s type is now CircuitUiState
instead of Any
.ScreenUi
is now removed and Ui.Factory
simply returns Ui
instances now.API Change: CircuitConfig.onUnavailableContent
is now no longer nullable. By default it displays a big ugly error text. If you want the previous behavior of erroring, replace it with a composable function that just throws an exception.
Dependency updates
Kotlin 1.8.0\nCompose-JB 1.3.0\nKSP 1.8.0-1.0.9\nCompose Runtime 1.3.3\nCompose UI 1.3.3\nCompose Animation 1.3.3\n
2022-12-22
ViewModel
s. This is now done automatically by the Circuit itself.circuit-retained
is now fully optional and not included as a transitive dependency of circuit-core. If you want to use it, see its installation instructions in its README.Screen
as @Immutable
.LocalCircuitOwner
is now just LocalCircuitConfig
to be more idiomatic.LocalRetainedStateRegistryOwner
is now just LocalRetainedStateRegistry
to be more idiomatic.Continuity
is now internal
and not publicly exposed since it no longer needs to be manually provided.ViewModelBackStackRecordLocalProvider
is now internal
and not publicly exposed since it no longer needs to be manually provided.[versions]\nanvil = \"2.4.3\"\ncompose-jb = \"1.2.2\"\ncompose-animation = \"1.3.2\"\ncompose-compiler = \"1.3.2\"\ncompose-foundation = \"1.3.1\"\ncompose-material = \"1.3.1\"\ncompose-material3 = \"1.0.1\"\ncompose-runtime = \"1.3.2\"\ncompose-ui = \"1.3.2\"\nkotlin = \"1.7.22\"\n
2022-12-07
Presenter
and Ui
factories\u2019 create()
functions now offer a CircuitContext
parameter in place of a CircuitConfig
parameter. This class contains a CircuitConfig
, a tagging API, and access to parent contexts. This allows for plumbing your own metadata through Circuit\u2019s internals such as tracing tools, logging, etc.EventListener
.onBeforeCreatePresenter
onAfterCreatePresenter
onBeforeCreateUi
onAfterCreateUi
onUnavailableContent
onStartPresent
onDisposePresent
onStartContent
onDisposeContent
dispose
1.3.1
.1.2.1
.0.6.1
.2022-11-07
onRootPop()
parameter in rememberCircuitNavigator()
but use LocalOnBackPressedDispatcherOwner
for backpress handling by default.2022-11-01
circuit-overlay
artifact.circuit-core
artifact now packages in baseline profiles.onRootPop()
option in rememberCircuitNavigator()
, instead you should install your own BackHandler()
prior to rendering your circuit content to customize back behavior when the circuit Navigator
is at root.circuit-codegen-annotations
is now a multiplatform project and doesn\u2019t accidentally impose the compose-desktop dependency.We\u2019ve also updated a number of docs around code gen, overlays, and interop (including a new interop sample).
"},{"location":"changelog/#022","title":"0.2.2","text":"2022-10-27
2022-10-27
2022-10-26
New: Code gen artifact. This targets specifically using Dagger + Anvil and will generate Presenter
and Ui.Factory
implementations for you. See CircuitInject
for more details.
ksp(\"com.slack.circuit:circuit-codegen:x.y.z\")\nimplementation(\"com.slack.circuit:circuit-codegen-annotations:x.y.z\")\n
New: There is now an EventListener
API for instrumenting state changes for a given Screen
. See its docs for more details.
rememberRetained
implementation and support for multiple variables. Previously it only worked with one variable.Dependency updates
androidx.activity 1.6.1\nandroidx.compose 1.3.0\nMolecule 0.5.0\n
"},{"location":"changelog/#012","title":"0.1.2","text":"2022-10-12
1.2.0
.0.12.0
.Presenter.test()
.2022-10-10
2022-10-10
Initial release, see the docs: https://slackhq.github.io/circuit/.
Note that this library is still under active development and not recommended for production use. We\u2019ll do a more formal announcement when that time comes!
"},{"location":"circuit-content/","title":"CircuitContent","text":"The simplest entry point of a Circuit screen is the composable CircuitContent
function. This function accepts a Screen
and automatically finds and pairs corresponding Presenter
and Ui
instances to render in it.
CircuitCompositionLocals(circuit) {\n CircuitContent(HomeScreen)\n}\n
This can be used for simple screens or as nested components of larger, more complex screens.
"},{"location":"circuitx/","title":"CircuitX","text":"CircuitX is a suite of extension artifacts for Circuit. These artifacts are intended to be batteries-included implementations of common use cases, such as out-of-the-box Overlay
types or Android navigation interop.
These packages differ from Circuit\u2019s core artifacts in a few ways:
com.slack.circuitx
package prefix.The circuitx-android
artifact contains Android-specific extensions for Circuit.
dependencies {\n implementation(\"com.slack.circuit:circuitx-android:<version>\")\n}\n
"},{"location":"circuitx/#navigation","title":"Navigation","text":"It can be important for Circuit to be able to navigate to Android targets, such as other activities or custom tabs. To support this, decorate your existing Navigator
instance with rememberAndroidScreenAwareNavigator()
.
class MainActivity : Activity {\n override fun onCreate(savedInstanceState: Bundle?) {\n setContent {\n val backStack = rememberSaveableBackStack(root = HomeScreen)\n val navigator = rememberAndroidScreenAwareNavigator(\n rememberCircuitNavigator(backstack), // Decorated navigator\n this@MainActivity\n )\n CircuitCompositionLocals(circuit) {\n NavigableCircuitContent(navigator, backstack)\n }\n }\n }\n}\n
rememberAndroidScreenAwareNavigator()
has two overloads - one that accepts a Context
and one that accepts an AndroidScreenStarter
. The former is just a shorthand for the latter that only supports IntentScreen
. You can also implement your own starter that supports other screen types.
AndroidScreen
is the base Screen
type that this navigator and AndroidScreenStarter
interact with. There is a built-in IntentScreen
implementation that wraps an Intent
and an options Bundle
to pass to startActivity()
. Custom AndroidScreens
can be implemented separately and route through here, but you should be sure to implement your own AndroidScreenStarter
to handle them accordingly.
CircuitX provides some effects for use with logging/analytics. These effects are typically used in Circuit presenters for tracking impressions
and will run only once until forgotten based on the current circuit-retained strategy.
dependencies {\n implementation(\"com.slack.circuit:circuitx-effects:<version>\")\n}\n
"},{"location":"circuitx/#impressioneffect","title":"ImpressionEffect","text":"ImpressionEffect
is a simple single fire side effect useful for logging or analytics. This impression
will run only once until it is forgotten based on the current RetainedStateRegistry
.
ImpressionEffect {\n // Impression \n}\n
"},{"location":"circuitx/#launchedimpressioneffect","title":"LaunchedImpressionEffect","text":"This is useful for async single fire side effects like logging or analytics. This effect will run a suspendable impression
once until it is forgotten based on the RetainedStateRegistry
.
LaunchedImpressionEffect {\n // Impression \n}\n
"},{"location":"circuitx/#rememberimpressionnavigator","title":"RememberImpressionNavigator","text":"A LaunchedImpressionEffect
that is useful for async single fire side effects like logging or analytics that need to be navigation aware. This will run the impression
again if it re-enters the composition after a navigation event.
val navigator = rememberImpressionNavigator(\n navigator = Navigator()\n) {\n // Impression\n}\n
"},{"location":"circuitx/#gesture-navigation","title":"Gesture Navigation","text":"CircuitX provides NavDecoration
implementation which support navigation through appropriate gestures on certain platforms.
dependencies {\n implementation(\"com.slack.circuit:circuitx-gesture-navigation:<version>\")\n}\n
To enable gesture navigation support, you can use the use the GestureNavigationDecoration
function:
NavigableCircuitContent(\n navigator = navigator,\n backStack = backstack,\n decoration = GestureNavigationDecoration(\n // Pop the back stack once the user has gone 'back'\n navigator::pop\n )\n)\n
"},{"location":"circuitx/#android_1","title":"Android","text":"On Android, this supports the Predictive back gesture which is available on Android 14 and later (API level 34+). On older platforms, Circuit\u2019s default NavDecoration
decoration is used instead.
On iOS, this simulates iOS\u2019s \u2018Interactive Pop Gesture\u2019 in Compose UI, allowing the user to swipe Circuit UIs away. As this is a simulation of the native behavior, it does not match the native functionality perfectly. However, it is a good approximation.
Tivi app running on iPhone"},{"location":"circuitx/#other-platforms","title":"Other platforms","text":"On other platforms we defer to Circuit\u2019s default NavDecoration
decoration.
CircuitX provides a few out-of-the-box Overlay
implementations that you can use to build common UIs.
dependencies {\n implementation(\"com.slack.circuit:circuitx-overlays:<version>\")\n}\n
"},{"location":"circuitx/#bottomsheetoverlay","title":"BottomSheetOverlay
","text":"BottomSheetOverlay
is an overlay that shows a bottom sheet with a strongly-typed API for the input model to the sheet content and result type. This allows you to easily use a bottom sheet to prompt for user input and suspend the underlying Circuit content until that result is returned.
/** A hypothetical bottom sheet of available actions when long-pressing a list item. */\nsuspend fun OverlayHost.showLongPressActionsSheet(): Action {\n return show(\n BottomSheetOverlay(\n model = listOfActions()\n ) { actions, overlayNavigator ->\n ActionsSheet(\n actions,\n overlayNavigator::finish // Finish the overlay with the clicked Action\n )\n }\n )\n}\n\n@Composable\nfun ActionsSheet(actions: List<Action>, onActionClicked: (Action) -> Unit) {\n Column {\n actions.forEach { action ->\n TextButton(onClick = { onActionClicked(action) }) {\n Text(action.title)\n }\n }\n }\n}\n
"},{"location":"circuitx/#dialog-overlays","title":"Dialog Overlays","text":"alertDialogOverlay
is function that returns an Overlay that shows a simple confirmation dialog with configurable inputs. This acts more or less as an Overlay
shim over the Material 3 AlertDialog
API.
/** A hypothetical confirmation dialog. */\nsuspend fun OverlayHost.showConfirmationDialog(): Action {\n return show(\n alertDialogOverlay(\n titleText = { Text(\"Are you sure?\") },\n confirmButton = { onClick -> Button(onClick = onClick) { Text(\"Yes\") } },\n dismissButton = { onClick -> Button(onClick = onClick) { Text(\"No\") } },\n )\n )\n}\n
There are also more generic BasicAlertDialog
and BasicDialog
implementations that allow more customization.
FullScreenOverlay
","text":"Sometimes it\u2019s useful to have a full-screen overlay that can be used to show a screen in full above the current content. This API is fairly simple to use and just takes a Screen
input of what content you want to show in the overlay.
overlayHost.showFullScreenOverlay(\n ImageViewerScreen(id = url, url = url, placeholderKey = name)\n)\n
When to use FullScreenOverlay
vs navigating to a Screen
?
While they achieve similar results, the key difference is that FullScreenOverlay
is inherently an ephemeral UI that is controlled by an underlying primary UI. It cannot navigate elsewhere and it does not participate in the backstack.
If using Dagger and Anvil or Hilt, Circuit offers a KSP-based code gen solution to ease boilerplate around generating factories.
"},{"location":"code-gen/#installation","title":"Installation","text":"plugins {\n id(\"com.google.devtools.ksp\")\n}\n\ndependencies {\n api(\"com.slack.circuit:circuit-codegen-annotations:<version>\")\n ksp(\"com.slack.circuit:circuit-codegen:<version>\")\n}\n
Note that Anvil is enabled by default. If you are using Hilt, you must specify the mode as a KSP arg.
ksp {\n arg(\"circuit.codegen.mode\", \"hilt\")\n}\n
If using Kotlin multiplatform with typealias annotations for Dagger annotations (i.e. expect annotations in common with actual typealias declarations in JVM source sets), you can match on just annotation short names alone to support this case via circuit.codegen.lenient
mode.
ksp {\n arg(\"circuit.codegen.lenient\", \"true\")\n}\n
"},{"location":"code-gen/#usage","title":"Usage","text":"The primary entry point is the CircuitInject
annotation.
This annotation is used to mark a UI or presenter class or function for code generation. When annotated, the type\u2019s corresponding factory will be generated and keyed with the defined screen
.
The generated factories are then contributed to Anvil via ContributesMultibinding
and scoped with the provided scope
key.
Presenter
and Ui
classes can be annotated and have their corresponding Presenter.Factory
or Ui.Factory
classes generated for them.
Presenter
@CircuitInject(HomeScreen::class, AppScope::class)\nclass HomePresenter @Inject constructor(...) : Presenter<HomeState> { ... }\n\n// Generates\n@ContributesMultibinding(AppScope::class)\nclass HomePresenterFactory @Inject constructor() : Presenter.Factory { ... }\n
UI
@CircuitInject(HomeScreen::class, AppScope::class)\nclass HomeUi @Inject constructor(...) : Ui<HomeState> { ... }\n\n// Generates\n@ContributesMultibinding(AppScope::class)\nclass HomeUiFactory @Inject constructor() : Ui.Factory { ... }\n
"},{"location":"code-gen/#functions","title":"Functions","text":"Simple functions can be annotated and have a corresponding Presenter.Factory
generated. This is primarily useful for simple cases where a class is just technical tedium.
Requirements - Presenter function names must end in Presenter
, otherwise they will be treated as UI functions. - Presenter functions must return a CircuitUiState
type. - UI functions can optionally accept a CircuitUiState
type as a parameter, but it is not required. - UI functions must return Unit
. - Both presenter and UI functions must be Composable
.
Presenter
@CircuitInject(HomeScreen::class, AppScope::class)\n@Composable\nfun HomePresenter(): HomeState { ... }\n\n// Generates\n@ContributesMultibinding(AppScope::class)\nclass HomePresenterFactory @Inject constructor() : Presenter.Factory { ... }\n
UI
@CircuitInject(HomeScreen::class, AppScope::class)\n@Composable\nfun Home(state: HomeState) { ... }\n*\n// Generates\n@ContributesMultibinding(AppScope::class)\nclass HomeUiFactory @Inject constructor() : Ui.Factory { ... }\n
"},{"location":"code-gen/#assisted-injection","title":"Assisted injection","text":"Any type that is offered in Presenter.Factory
and Ui.Factory
can be offered as an assisted injection to types using Dagger AssistedInject
. For these cases, the AssistedFactory
-annotated interface should be annotated with CircuitInject
instead of the enclosing class.
Types available for assisted injection are:
Screen
\u2013 the screen key used to create the Presenter
or Ui
.Navigator
\u2013 (presenters only)Circuit
Each should only be defined at-most once.
Examples
// Function example\n@CircuitInject(HomeScreen::class, AppScope::class)\n@Composable\nfun HomePresenter(screen: Screen, navigator: Navigator): HomeState { ... }\n\n// Class example\nclass HomePresenter @AssistedInject constructor(\n @Assisted screen: Screen,\n @Assisted navigator: Navigator,\n ...\n) : Presenter<HomeState> {\n // ...\n @CircuitInject(HomeScreen::class, AppScope::class)\n @AssistedFactory\n fun interface Factory {\n fun create(screen: Screen, navigator: Navigator, context: CircuitContext): HomePresenter\n }\n}\n
"},{"location":"code-of-conduct/","title":"Code of Conduct","text":""},{"location":"code-of-conduct/#introduction","title":"Introduction","text":"Diversity and inclusion make our community strong. We encourage participation from the most varied and diverse backgrounds possible and want to be very clear about where we stand.
Our goal is to maintain a safe, helpful and friendly community for everyone, regardless of experience, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, nationality, or other defining characteristic.
This code and related procedures also apply to unacceptable behavior occurring outside the scope of community activities, in all community venues (online and in-person) as well as in all one-on-one communications, and anywhere such behavior has the potential to adversely affect the safety and well-being of community members.
For more information on our code of conduct, please visit https://slackhq.github.io/code-of-conduct
"},{"location":"contributing/","title":"Contributors Guide","text":"Note that this project is considered READ-ONLY. You are welcome to discuss or ask questions in the discussions section of the repo, but we do not normally accept external contributions without prior discussion.
"},{"location":"contributing/#development","title":"Development","text":"Check out this repo with Android Studio. It\u2019s a standard gradle project and conventional to checkout.
Circuit is a Kotlin Multiplatform project, so ensure you have your environment set up accordingly: https://www.jetbrains.com/help/kotlin-multiplatform-dev/multiplatform-setup.html
The primary project is circuit
. The primary sample is samples/star
.
This project is written in Kotlin and should only use Kotlin.
Code formatting is checked via Spotless. To run the formatter, use the spotlessApply
command.
./gradlew spotlessApply\n
"},{"location":"contributing/#ios","title":"iOS","text":"To build any of the iOS checks, you must do the following: 1. Run bundle install
to set up fastlane. 2. Have swiftformat
installed. You can install it via brew install swiftformat
.
At its core, Circuit works on the Factory pattern. Every Presenter
and Ui
is contributed to a Circuit
instance by a corresponding factory that creates them for given Screen
s. These are intended to be aggregated in the DI layer and added to a Circuit
instance during construction.
val circuit = Circuit.Builder()\n .addUiFactory(FavoritesUiFactory())\n .addPresenterFactory(FavoritesPresenterFactory())\n .build()\n
Look familiar?
If you\u2019ve used Moshi or Retrofit, these should feel fairly familiar!
Presenter factories can be generated or hand-written, depending on if they aggregate an entire screen or are simple one-offs. Presenters are also given access to the current Navigator in this level.
class FavoritesScreenPresenterFactory @Inject constructor(\n private val favoritesPresenterFactory: FavoritesPresenter.Factory,\n) : Presenter.Factory {\n override fun create(screen: Screen, navigator: Navigator, context: CircuitContext): Presenter<*>? {\n return when (screen) {\n is FavoritesScreen -> favoritesPresenterFactory.create(screen, navigator, context)\n else -> null\n }\n }\n}\n
UI factories are similar, but generally should not aggregate other UIs unless there\u2019s a DI-specific reason to do so (which there usually isn\u2019t!).
class FavoritesScreenUiFactory @Inject constructor() : Ui.Factory {\n override fun create(screen: Screen, context: CircuitContext): Ui<*>? {\n return when (screen) {\n is FavoritesScreen -> favoritesUi()\n else -> null\n }\n }\n}\n\nprivate fun favoritesUi() = ui<State> { state, modifier -> Favorites(state, modifier) }\n
Info
Note how these include a Modifier
. You should pass on these modifiers to your UI. Always provide a modifier!
We canonically write these out as a separate function (favoritesUi()
) that returns a Ui
, which in turn calls through to the real (basic) Compose UI function (Favorites()
). This ensures our basic compose functions are top-level and accessible by tests, and also discourages storing anything in class members rather than idiomatic composable state vars. If you use code gen, it handles the intermediate function for you.
Circuit can interop anywhere that Compose can interop. This includes common cases like Android Views
, RxJava, Kotlin Flow
, and more.
Presenter
","text":"Lean on first-party interop-APIs where possible! See examples of interop with different libraries in the :samples:interop
project.
UI
","text":""},{"location":"interop/#ui-view","title":"Ui
-> View
","text":"Just embed the Circuit in a ComposeView
like any other Compose UI.
View
-> Ui
","text":"You can wrap your view in an AndroidView
in a custom Ui
implementation.
class ExistingCustomViewUi : Ui<State> {\n @Composable\n fun Content(state: State, modifier: Modifier = Modifier) {\n AndroidView(\n modifier = ...\n factory = { context ->\n ExistingCustomView(context)\n },\n update = { view ->\n view.setState(state)\n view.setOnClickListener { state.eventSink(Event.Click) }\n }\n }\n}\n
"},{"location":"navigation/","title":"Navigation","text":"For navigable contents, we have a custom compose-based backstack implementation that the androidx folks shared with us. Navigation becomes two parts:
BackStack
, where we use a SaveableBackStack
implementation that saves a stack of Screen
s and the ProvidedValues
for each record on that stack (allowing us to save and restore on configuration changes automatically).Navigator
, which is a simple interface that we can point at a BackStack
and offers simple goTo(<screen>)
/pop()
semantics. These are offered to presenters to perform navigation as needed to other screens.A new navigable content surface is handled via the NavigableCircuitContent
functions.
setContent {\n val backStack = rememberSaveableBackStack(root = HomeScreen)\n val navigator = rememberCircuitNavigator(backStack)\n NavigableCircuitContent(navigator, backStack)\n}\n
Warning
SaveableBackStack
must have a size of 1 or more after initialization. It\u2019s an error to have a backstack with zero items.
Presenters are then given access to these navigator instances via Presenter.Factory
(described in Factories), which they can save if needed to perform navigation.
fun showAddFavorites() {\n navigator.goTo(\n AddFavorites(\n externalId = uuidGenerator.generate()\n )\n )\n}\n
If you want to have custom behavior for when back is pressed on the root screen (i.e. backstack.size == 1
), you should implement your own BackHandler
and use it before creating the backstack.
setContent {\n val backStack = rememberSaveableBackStack(root = HomeScreen)\n BackHandler(onBack = { /* do something on root */ })\n // The Navigator's internal BackHandler will take precedence until it is at the root screen.\n val navigator = rememberCircuitNavigator(backstack)\n NavigableCircuitContent(navigator, backstack)\n}\n
"},{"location":"navigation/#results","title":"Results","text":"In some cases, it makes sense for a screen to return a result to the previous screen. This is done by using the an answering Navigator pattern in Circuit.
The primary entry point for requesting a result is the rememberAnsweringNavigator
API, which takes a Navigator
or BackStack
and PopResult
type and returns a navigator that can go to a screen and await a result.
Result types must implement PopResult
and are used to carry data back to the previous screen.
The returned navigator should be used to navigate to the screen that will return the result. The target screen can then pop
the result back to the previous screen and Circuit will automatically deliver this result to the previous screen\u2019s receiver.
var photoUri by remember { mutableStateOf<String?>(null) }\nval takePhotoNavigator = rememberAnsweringNavigator<TakePhotoScreen.Result>(navigator) { result ->\n photoUri = result.uri\n}\n\n// Elsewhere\ntakePhotoNavigator.goTo(TakePhotoScreen)\n\n// In TakePhotoScreen.kt\ndata object TakePhotoScreen : Screen {\n @Parcelize\n data class Result(val uri: String) : PopResult\n}\n\nclass TakePhotoPresenter {\n @Composable fun present(): State {\n // ...\n navigator.pop(result = TakePhotoScreen.Result(photoUri))\n }\n}\n
Circuit automatically manages saving/restoring result states and ensuring that results are only delivered to the original receiver that requested it. If the target screen does not pop back a result, the previous screen\u2019s receiver will just never receive one.
When to use an Overlay
vs navigating to a Screen
with result?
See this doc in Overlays!
"},{"location":"navigation/#nested-navigation","title":"Nested Navigation","text":"Navigation carries special semantic value in CircuitContent
as well, where it\u2019s common for UIs to want to curry navigation events emitted by nested UIs. For this case, there\u2019s a CircuitContent
overload that accepts an optional onNavEvent callback that you must then forward to a Navigator instance.
@Composable fun ParentUi(state: ParentState, modifier: Modifier = Modifier) {\n CircuitContent(NestedScreen, modifier = modifier, onNavEvent = { navEvent -> state.eventSink(NestedNav(navEvent)) })\n}\n\n@Composable fun ParentPresenter(navigator: Navigator): ParentState {\n return ParentState(...) { event ->\n when (event) {\n is NestedNav -> navigator.onNavEvent(event.navEvent)\n }\n }\n}\n\n@Composable \nfun NestedPresenter(navigator: Navigator): NestedState {\n // These are forwarded up!\n navigator.goTo(AnotherScreen)\n\n // ...\n}\n
"},{"location":"overlays/","title":"Overlays","text":"The circuit-overlay
artifact contains an optional API for presenting overlays on top of the current UI.
@Composable\nfun SubmitAnswer(state: FormState, modifier: Modifier = Modifier) {\n if (state.promptConfirmation) {\n OverlayEffect { host ->\n // Suspend on the result of the overlay\n val result = host.show(ConfirmationDialogOverlay(title = \"Are you sure?\"))\n state.eventSink(SubmitAnswerEvent(result))\n }\n }\n}\n
"},{"location":"overlays/#usage","title":"Usage","text":"The core APIs are the Overlay
and OverlayHost
interfaces.
An Overlay
is composable content that can be shown on top of other content via an OverlayHost
. Overlays are typically used for one-off request/result flows and should not usually attempt to do any sort of external navigation or make any assumptions about the state of the app. They should only emit a Result
to the given OverlayNavigator
parameter when they are done.
interface Overlay<Result : Any> {\n @Composable\n fun Content(navigator: OverlayNavigator<Result>)\n}\n
For common overlays, it\u2019s useful to create a common Overlay
subtype that can be reused. For example: BottomSheetOverlay
, ModalOverlay
, TooltipOverlay
, etc.
An OverlayHost
is provided via composition local and exposes a suspend show()
function to show an overlay and resume with a typed Result
.
val result = LocalOverlayHost.current.show(BottomSheetOverlay(...))\n
Where BottomSheetOverlay
is a custom bottom sheet implementation of an Overlay
.
In composition, you can also use OverlayEffect
for a more streamlined API.
OverlayEffect { host ->\n val result = host.show(BottomSheetOverlay(...))\n}\n
"},{"location":"overlays/#installation","title":"Installation","text":"Add the dependency.
implementation(\"com.slack.circuit:circuit-overlay:$circuit_version\")\n
The simplest starting point for adding overlay support is the ContentWithOverlays
composable function.
ContentWithOverlays {\n // Your content here\n}\n
This will expose a LocalOverlayHost
composition local that can be used by UIs to show overlays. This also exposes a LocalOverlayState
composition local that can be used to check the current overlay state (UNAVAILABLE
, HIDDEN
, or SHOWING
).
Overlay
vs PopResult
","text":"Overlays and navigation results can accomplish similar goals, and you should choose the right one for your use case.
Overlay
PopResult
Survives process death \u274c \u2705 Type-safe \u2705 \ud83d\udfe1 Suspend on result \u2705 \u274c Participates in back stack \u274c \u2705 Supports non-saveable inputs/outputs \u2705 \u274c Can participate with the caller\u2019s UI \u2705 \u274c Can return multiple different result types \u274c \u2705 Works without a back stack \u2705 \u274c *PopResult
is technically type-safe, but it\u2019s not as strongly typed as Overlay
results since there is nothing inherently requiring the target screen to pop a given result type back.
The core Presenter interface is this:
interface Presenter<UiState : CircuitUiState> {\n @ComposableTarget(\"presenter\")\n @Composable\n fun present(): UiState\n}\n
Presenters are solely intended to be business logic for your UI and a translation layer in front of your data layers. They are generally Dagger-injected types as the data layers they interpret are usually coming from the DI graph. In simple cases, they can be typed as a simple @Composable
presenter function allowing Circuit code gen to generate the corresponding interface and factory for you.
A very simple presenter can look like this:
class FavoritesPresenter(...) : Presenter<State> {\n @Composable override fun present(): State {\n var favorites by remember { mutableStateOf(<initial>) }\n\n return State(favorites) { event -> ... }\n }\n}\n
In this example, the present()
function simply computes a state
and returns it. If it has UI events to handle, an eventSink: (Event) -> Unit
property should be exposed in the State
type it returns.
With DI, the above example becomes something more like this:
class FavoritesPresenter @AssistedInject constructor(\n @Assisted private val screen: FavoritesScreen,\n @Assisted private val navigator: Navigator,\n private val favoritesRepository: FavoritesRepository\n) : Presenter<State> {\n @Composable override fun present(): State {\n // ...\n }\n @AssistedFactory\n fun interface Factory {\n fun create(screen: FavoritesScreen, navigator: Navigator, context: CircuitContext): FavoritesPresenter\n }\n}\n
Assisted injection allows passing on the screen
and navigator
from the relevant Presenter.Factory
to this presenter for further reference.
When dealing with nested presenters, a presenter could bypass implementing a class entirely by simply being written as a function that other presenters can use.
// From cashapp/molecule's README examples\n@Composable\nfun ProfilePresenter(\n userFlow: Flow<User>,\n balanceFlow: Flow<Long>,\n): ProfileModel {\n val user by userFlow.collectAsState(null)\n val balance by balanceFlow.collectAsState(0L)\n\n return if (user == null) {\n Loading\n } else {\n Data(user.name, balance)\n }\n}\n
Presenters can present other presenters by injecting their assisted factories/providers, but note that this makes them a composite presenter that is now assuming responsibility for managing state of multiple nested presenters.
"},{"location":"presenter/#no-compose-ui","title":"No Compose UI","text":"Presenter logic should not emit any Compose UI. They are purely for presentation business logic. To help enforce this, Presenter.present
is annotated with @ComposableTarget(\"presenter\")
. This helps prevent use of Compose UI in the presentation logic as the compiler will emit a warning if you do.
Tip
This warning does not appear in the IDE, so it\u2019s recommended to use allWarningsAsErrors
in your build configuration to fail the build on this event.
// In build.gradle.kts\nkotlin.compilerOptions.allWarningsAsErrors.set(true)\n
"},{"location":"presenter/#retention","title":"Retention","text":"There are three types of composable retention functions used in Circuit.
remember
\u2013 from Compose, remembers a value across recompositions. Can be any type.rememberRetained
\u2013 custom, remembers a value across recompositions, the back stack, and configuration changes. Can be any type, but should not retain leak-able things like Navigator
instances or Context
instances. Backed by a hidden ViewModel
on Android.rememberSaveable
\u2013 from Compose, remembers a value across recompositions, the back stack, configuration changes, and process death. Must be a primitive, Parcelable
(on Android), or implement a custom Saver
. This should not retain leakable things like Navigator
instances or Context
instances and is backed by the framework saved instance state system.Developers should use the right tool accordingly depending on their use case. Consider these three examples.
The first one will preserve the count
value across recompositions, but not the back stack, configuration changes, or process death.
@Composable\nfun CounterPresenter(): CounterState {\n var count by remember { mutableStateOf(0) }\n\n return CounterState(count) { event ->\n when (event) {\n is CounterEvent.Increment -> count++\n is CounterEvent.Decrement -> count--\n }\n }\n}\n
The second one will preserve the state across recompositions, the back stack, and configuration changes, but not process death.
@Composable\nfun CounterPresenter(): CounterState {\n var count by rememberRetained { mutableStateOf(0) }\n\n return CounterState(count) { event ->\n when (event) {\n is CounterEvent.Increment -> count++\n is CounterEvent.Decrement -> count--\n }\n }\n}\n
The third case will preserve the count
state across recompositions, the back stack, configuration changes, and process death.
@Composable\nfun CounterPresenter(): CounterState {\n var count by rememberSaveable { mutableStateOf(0) }\n\n return CounterState(count) { event ->\n when (event) {\n is CounterEvent.Increment -> count++\n is CounterEvent.Decrement -> count--\n }\n }\n}\n
remember
rememberRetained
rememberSaveable
Recompositions \u2705 \u2705 \u2705 Back stack \u274c \u2705* \u2705* Configuration changes (Android) \u274c \u2705 \u2705 Process death \u274c \u274c \u2705 Can be non-Saveable types \u2705 \u2705 \u274c *If using NavigableCircuitContent
\u2019s default configuration.
Screens are keys for Presenter and UI pairings.
The core Screen
interface is this:
interface Screen : Parcelable\n
These types are Parcelable
on Android for saveability in our backstack and easy deeplinking. A Screen
can be a simple marker data object
or a data class
with information to pass on.
@Parcelize\ndata object HomeScreen : Screen\n\n@Parcelize\ndata class AddFavoritesScreen(val externalId: UUID) : Screen\n
These are used by Navigator
s (when called from presenters) or CircuitContent
(when called from UIs) to start a new sub-circuit or nested circuit.
// In a presenter class\nfun showAddFavorites() {\n navigator.goTo(\n AddFavoritesScreen(\n externalId = uuidGenerator.generate()\n )\n )\n}\n
The information passed into a screen can also be used to interact with the data layer. In the example here, we are getting the externalId
from the screen in order to get information back from our repository.
// In a presenter class\nclass AddFavoritesPresenter\n@AssistedInject\nconstructor(\n @Assisted private val screen: AddFavoritesScreen,\n private val favoritesRepository: FavoritesRepository,\n) : Presenter<AddFavoritesScreen.State> {\n @Composable\n override fun present() : AddFavoritesScreen.State {\n val favorite = favoritesRepository.getFavorite(screen.externalId)\n // ...\n }\n}\n
Screens are also used to look up those corresponding components in Circuit
.
val presenter: Presenter<*>? = circuit.presenter(addFavoritesScreen, navigator)\nval ui: Ui<*>? = circuit.ui(addFavoritesScreen)\n
Nomenclature
Semantically, in this example we would call all of these components together the \u201cAddFavorites Screen\u201d.
"},{"location":"setup/","title":"Setting up Circuit","text":"Setting up Circuit is a breeze! Just add the following to your build:
"},{"location":"setup/#installation","title":"Installation","text":"The simplest way to get up and running is with the circuit-foundation
dependency, which includes all the core Circuit artifacts.
dependencies {\n implementation(\"com.slack.circuit:circuit-foundation:<version>\")\n}\n
"},{"location":"setup/#setup","title":"Setup","text":"Create a Circuit
instance. This controls all your common configuration, Presenter/Ui factories, etc.
val circuit = Circuit.Builder()\n .addUiFactory(AddFavoritesUiFactory())\n .addPresenterFactory(AddFavoritesPresenterFactory())\n .build()\n
This configuration can be rebuilt via newBuilder()
and usually would live in your program\u2019s DI graph.
Once you have a configuration ready, the simplest way to get going with Circuit is via CircuitCompositionLocals
. This automatically exposes the config to all child Circuit composables and allows you to get off the ground quickly with CircuitContent
, NavigableCircuitContent
, etc.
CircuitCompositionLocals(circuit) {\n CircuitContent(AddFavoritesScreen())\n}\n
See the docs for CircuitContent
and NavigableCircuitContent
for more information.
Circuit is split into a few different artifacts to allow for more granular control over your dependencies. The following table shows the available artifacts and their purpose:
Artifact ID Dependenciescircuit-backstack
Circuit\u2019s backstack implementation. circuit-runtime
Common runtime components like Screen
, Navigator
, etc. circuit-runtime-presenter
The Presenter
API, depends on circuit-runtime
. circuit-runtime-ui
The Ui
API, depends on circuit-runtime
. circuit-foundation
The Circuit foundational APIs like Circuit
, CircuitContent
, etc. Depends on the first three. circuit-test
First-party test APIs for testing navigation, state emissions, and event sinks. circuit-overlay
Optional Overlay
APIs. circuit-retained
Optional rememberRetained()
APIs."},{"location":"setup/#platform-support","title":"Platform Support","text":"Circuit is a multiplatform library, but not all features are available on all platforms. The following table shows which features are available on which platforms:
Backstack
\u2705 \u2705 \u2705 \u2705 CircuitContent
\u2705 \u2705 \u2705 \u2705 ContentWithOverlays
\u2705 \u2705 \u2705 \u2705 NavigableCircuitContent
\u2705 \u2705 \u2705 \u2705 Navigator
\u2705 \u2705 \u2705 \u2705 SaveableBackstack
\u2705 \u2705 \u2705 \u2705 Saveable is a no-op on non-android. rememberCircuitNavigator
\u2705 \u2705 \u2705 \u2705 rememberRetained
\u2705 \u2705 \u2705 \u2705 TestEventSink
\u2705 \u2705 \u2705 \u2705 On JS you must use asEventSinkFunction()
."},{"location":"states-and-events/","title":"States and Events","text":"The core state and event interfaces in Circuit are CircuitUiState
and CircuitUiEvent
. All state and event types should implement/extend these marker interfaces.
Presenters are simple functions that determine and return composable states. UIs are simple functions that render states. Uis can emit events via eventSink
properties in state classes, which presenters then handle. These are the core building blocks!
States should be @Stable
; events should be @Immutable
.
Wait, event callbacks in state types?
Yep! This may feel like a departure from how you\u2019ve written UDF patterns in the past, but we really like it. We tried different patterns before with event Flow
s and having Circuit internals manage these for you, but we found they came with tradeoffs and friction points that we could avoid by just treating event emissions as another aspect of state. The end result is a tidier structure of state + event flows.
Flow
for events, which comes with caveats in compose (wrapping operators in remember
calls, pipelining nested event flows, etc)Click
may not make sense for Loading
states).Channel
and multicasting event streams.Flow
).Note
Currently, while functions are treated as implicitly Stable
by the compose compiler, they\u2019re not skippable when they\u2019re non-composable Unit-returning lambdas with equal-but-unstable captures. This may change though, and would be another free benefit for this case.
A longer-form writeup can be found in this PR.
"},{"location":"testing/","title":"Testing","text":"Circuit is designed to make testing as easy as possible. Its core components are not mockable nor do they need to be mocked. Fakes are provided where needed, everything else can be used directly.
Circuit will have a test artifact containing APIs to aid testing both presenters and composable UIs:
Presenter.test()
- an extension function that bridges the Compose and coroutines world. Use of this function is recommended for testing presenter state emissions and incoming UI events. Under the hood it leverages Molecule and Turbine.FakeNavigator
- a test fake implementing the Navigator
interface. Use of this object is recommended when testing screen navigation (ie. goTo, pop/back). This acts as a real navigator and exposes recorded information for testing purposes.TestEventSink
- a generic test fake for recording and asserting event emissions through an event sink function.Test helpers are available via the circuit-test
artifact.
testImplementation(\"com.slack.circuit:circuit-test:<version>\")\n
For Gradle JVM projects, you can use Gradle test fixtures syntax on the core circuit artifact.
testImplementation(testFixtures(\"com.slack.circuit:circuit:<version>\"))\n
"},{"location":"testing/#example","title":"Example","text":"Testing a Circuit Presenter and UI is a breeze! Consider the following example:
data class Favorite(id: Long, ...)\n\n@Parcelable\ndata object FavoritesScreen : Screen {\n sealed interface State : CircuitUiState {\n data object Loading : State\n data object NoFavorites : State\n data class Results(\n val list: List<Favorite>,\n val eventSink: (Event) -> Unit\n ) : State\n }\n\n sealed interface Event : CircuitUiEvent {\n data class ClickFavorite(id: Long): Event\n }\n}\n\nclass FavoritesPresenter @Inject constructor(\n navigator: Navigator,\n repo: FavoritesRepository\n) : Presenter<State> {\n @Composable override fun present(): State {\n val favorites by produceState<List<Favorites>?>(null) {\n value = repo.getFavorites()\n }\n\n return when {\n favorites == null -> Loading\n favorites.isEmpty() -> NoFavorites\n else ->\n Results(favorites) { event ->\n when (event) {\n is ClickFavorite -> navigator.goTo(FavoriteScreen(event.id))\n }\n }\n }\n }\n}\n\n@Composable\nfun FavoritesList(state: FavoritesScreen.State) {\n when (state) {\n Loading -> Text(text = stringResource(R.string.loading_favorites))\n NoFavorites -> Text(\n modifier = Modifier.testTag(\"no favorites\"),\n text = stringResource(R.string.no_favorites)\n )\n is Results -> {\n Text(text = \"Your Favorites\")\n LazyColumn {\n items(state.list) { Favorite(it, state.eventSink) }\n }\n }\n }\n}\n\n@Composable\nprivate fun Favorite(favorite: Favorite, eventSink: (FavoritesScreen.Event) -> Unit) {\n Row(\n modifier = Modifier.testTag(\"favorite\"),\n onClick = { eventSink(ClickFavorite(favorite.id)) }\n ) {\n Image(\n drawable = favorite.drawable, \n contentDescription = stringResource(R.string.favorite_image_desc)\n )\n Text(text = favorite.name)\n Text(text = favorite.date)\n }\n}\n
"},{"location":"testing/#presenter-unit-tests","title":"Presenter Unit Tests","text":"Here\u2019s a test to verify presenter emissions using the Presenter.test()
helper. This function acts as a shorthand over Molecule + Turbine to give you a ReceiveTurbine.() -> Unit
lambda.
@Test \nfun `present - emit loading state then list of favorites`() = runTest {\n val favorites = listOf(Favorite(1L, ...))\n\n val repo = TestFavoritesRepository(favorites)\n val presenter = FavoritesPresenter(navigator, repo)\n\n presenter.test {\n assertThat(awaitItem()).isEqualTo(FavoritesScreen.State.Loading)\n val resultsItem = awaitItem() as Results\n assertThat(resultsItem.favorites).isEqualTo(favorites)\n }\n}\n
The same helper can be used when testing how the presenter responds to incoming events:
@Test \nfun `present - navigate to favorite screen`() = runTest {\n val repo = TestFavoritesRepository(Favorite(123L))\n val presenter = FavoritesPresenter(navigator, repo)\n\n presenter.test {\n assertThat(awaitItem()).isEqualTo(FavoritesScreen.State.Loading)\n val resultsItem = awaitItem() as Results\n assertThat(resultsItem.favorites).isEqualTo(favorites)\n val clickFavorite = FavoriteScreen.Event.ClickFavorite(123L)\n\n // simulate user tapping favorite in UI\n resultsItem.eventSink(clickFavorite)\n\n assertThat(navigator.awaitNextScreen()).isEqualTo(FavoriteScreen(clickFavorite.id))\n }\n}\n
"},{"location":"testing/#android-ui-instrumentation-tests","title":"Android UI Instrumentation Tests","text":"UI tests can be driven directly through ComposeTestRule
and use its Espresso-esque API for assertions:
Here is also a good place to use a TestEventSink
and assert expected event emissions from specific UI interactions.
@Test\nfun favoritesList_show_favorites_for_result_state() = runTest {\n val favorites = listOf(Favorite(1L, ...))\n val events = TestEventSink<FavoriteScreen.Event>()\n\n composeTestRule.run {\n setContent { \n // bootstrap the UI in the desired state\n FavoritesList(\n state = FavoriteScreen.State.Results(favorites, events)\n )\n }\n\n onNodeWithTag(\"no favorites\").assertDoesNotExist()\n onNodeWithText(\"Your Favorites\").assertIsDisplayed()\n onAllNodesWithTag(\"favorite\").assertCountEquals(1)\n .get(1)\n .performClick()\n\n events.assertEvent(FavoriteScreen.Event.ClickFavorite(1L))\n }\n}\n
"},{"location":"testing/#snapshot-tests","title":"Snapshot Tests","text":"Because Circuit UIs simply take an input state parameter, snapshot tests via Paparazzi or Roborazzi are a breeze.
This allows allows you to render UI without a physical device or emulator and assert pixel-perfection on the result.
@Test\nfun previewFavorite() {\n paparazzi.snapshot { PreviewFavorite() }\n}\n
These are easy to maintain and review in GitHub.
Another neat idea is we think this will make it easy to stand up compose preview functions for IDE use and reuse them.
// In your main source\n@Preview\n@Composable\ninternal fun PreviewFavorite() {\n Favorite()\n}\n\n// In your unit test\n@Test\nfun previewFavorite() {\n paparazzi.snapshot { PreviewFavorite() }\n}\n
"},{"location":"tutorial/","title":"Tutorial","text":"This tutorial will help you ramp up to Circuit with a simple email app.
Note this assumes some prior experience with Compose. See these resources for more information:
You can do this tutorial in one of two ways:
"},{"location":"tutorial/#1-build-out-of-the-tutorial-sample","title":"1. Build out of thetutorial
sample","text":"Clone the circuit repo and work out of the :samples:tutorial
module. This has all your dependencies set up and ready to go, along with some reusable common code to save you some boilerplate. You can see an implementation of this tutorial there as well.
This can be run on Android or Desktop. - The Desktop entry point is main.kt
. To run the main function, you can run ./gradlew :samples:tutorial:run
. - The Android entry point is MainActivity
. Run ./gradlew :samples:tutorial:installDebug
to install it on a device or emulator.
First, set up Compose in your project. See the following guides for more information:
Next, add the circuit-foundation
dependency. This includes all the core Circuit artifacts.
dependencies {\n implementation(\"com.slack.circuit:circuit-foundation:<version>\")\n}\n
See setup docs for more information.
"},{"location":"tutorial/#create-a-screen","title":"Create aScreen
","text":"The primary entry points in Circuit are Screen
s (docs). These are the navigational building blocks of your app. A Screen
is a simple data class or data object that represents a unique location in your app. For example, a Screen
could represent an inbox list, an email detail, or a settings screen.
Let\u2019s start with a simple Screen
that represents an inbox list:
@Parcelize\ndata object InboxScreen : Screen\n
data object InboxScreen : Screen\n
Tip
Screen
is Parcelable
on Android. You should use the Parcelize plugin to annotate your screens with @Parcelize
.
Next, let\u2019s define some state for our InboxScreen
. Circuit uses unidirectional data flow (UDF) to ensure strong separation between presentation logic and UI. States should be stable or immutable, and directly renderable by your UIs. As such, you should design them to be as simple as possible.
Conventionally, this is written as a nested State
class inside your Screen
and must extend CircuitUiState
(docs).
data object InboxScreen : Screen {\n data class State(\n val emails: List<Email>\n ) : CircuitUiState\n}\n
Email.kt@Immutable\ndata class Email(\n val id: String,\n val subject: String,\n val body: String,\n val sender: String,\n val timestamp: String,\n val recipients: List<String>,\n)\n
See the states and events guide for more information.
"},{"location":"tutorial/#create-your-ui","title":"Create your UI","text":"InboxNext, let\u2019s define a Ui
for our InboxScreen
. A Ui
is a simple composable function that takes State
and Modifier
parameters.
It\u2019s responsible for rendering the state. You should write this like a standard composable. In this case, we\u2019ll use a LazyColumn
to render a list of emails.
@Composable\nfun Inbox(state: InboxScreen.State, modifier: Modifier = Modifier) {\n Scaffold(modifier = modifier, topBar = { TopAppBar(title = { Text(\"Inbox\") }) }) { innerPadding ->\n LazyColumn(modifier = Modifier.padding(innerPadding)) {\n items(state.emails) { email ->\n EmailItem(email)\n }\n }\n }\n}\n\n// Write one or use EmailItem from ui.kt\n@Composable\nprivate fun EmailItem(email: Email, modifier: Modifier = Modifier) {\n // ...\n}\n
For more complex UIs with dependencies, you can create a class that implements the Ui
interface (docs). This is rarely necessary though, and we won\u2019t use this in the tutorial.
class InboxUi(...) : Ui<InboxScreen.State> {\n @Composable\n override fun Content(state: InboxScreen.State, modifier: Modifier) {\n LazyColumn(modifier = modifier) {\n items(state.emails) { email ->\n EmailItem(email)\n }\n }\n }\n}\n
"},{"location":"tutorial/#implement-your-presenter","title":"Implement your presenter","text":"Next, let\u2019s define a Presenter
(docs) for our InboxScreen
. Circuit presenters are responsible for computing and emitting state.
class InboxPresenter : Presenter<InboxScreen.State> {\n @Composable\n override fun present(): InboxScreen.State {\n return InboxScreen.State(\n emails = listOf(\n Email(\n id = \"1\",\n subject = \"Meeting re-sched!\",\n body = \"Hey, I'm going to be out of the office tomorrow. Can we reschedule?\",\n sender = \"Ali Connors\",\n timestamp = \"3:00 PM\",\n recipients = listOf(\"all@example.com\"),\n ),\n // ... more emails\n )\n )\n }\n}\n
This is a trivial implementation that returns a static list of emails. In a real app, you\u2019d likely fetch this data from a repository or other data source. In our tutorial code in the repo, we\u2019ve added a simple EmailRepository
that you can use to fetch emails. It exposes a suspending getEmails()
function that returns a list of emails.
This is also a good opportunity to see where using compose in our presentation logic shines, as we can use Compose\u2019s advanced state management to make our presenter logic more expressive and easy to understand.
InboxScreen.ktclass InboxPresenter(private val emailRepository: EmailRepository) : Presenter<InboxScreen.State> {\n @Composable\n override fun present(): InboxScreen.State {\n val emails by produceState<List<Email>>(initialValue = emptyList()) {\n value = emailRepository.getEmails()\n }\n // Or a flow!\n // val emails by emailRepository.getEmailsFlow().collectAsState(initial = emptyList())\n return InboxScreen.State(emails)\n }\n}\n
Analogous to Ui
, you can also define simple/dependency-less presenters as just a top-level function.
@Composable\nfun InboxPresenter(): InboxScreen.State {\n val emails = ...\n return InboxScreen.State(emails)\n}\n
Tip
Generally, Circuit presenters are implemented as classes and Circuit UIs are implemented as top-level functions. You can mix and match as needed for a given use case. Under the hood, Circuit will wrap all top-level functions into a class for you.
"},{"location":"tutorial/#wiring-it-up","title":"Wiring it up","text":"Now that we have a Screen
, State
, Ui
, and Presenter
, let\u2019s wire them up together. Circuit accomplishes this with the Circuit
class (docs), which is responsible for connecting screens to their corresponding presenters and UIs. These are created with a simple builder pattern.
val emailRepository = EmailRepository()\nval circuit: Circuit =\n Circuit.Builder()\n .addPresenter<InboxScreen, InboxScreen.State>(InboxPresenter(emailRepository))\n .addUi<InboxScreen, InboxScreen.State> { state, modifier -> Inbox(state, modifier) }\n .build()\n
This instance should usually live on your application\u2019s DI graph.
Note
This is a simple example that uses the addPresenter
and addUi
functions. In a real app, you\u2019d likely use a Presenter.Factory
and Ui.Factory
to create your presenters and UIs dynamically.
Once you have this instance, you can plug it into CircuitCompositionLocals
(docs) and be on your way. This is usually a one-time setup in your application at its primary entry point.
class MainActivity {\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(savedInstanceState)\n val circuit = Circuit.Builder()\n // ...\n .build()\n\n setContent {\n CircuitCompositionLocals(circuit) {\n // ...\n }\n }\n }\n}\n
main.ktfun main() {\n val circuit = Circuit.Builder()\n // ...\n .build()\n\n application {\n Window(title = \"Inbox\", onCloseRequest = ::exitApplication) {\n CircuitCompositionLocals(circuit) {\n // ...\n }\n }\n }\n}\n
main.ktfun main() {\n val circuit = Circuit.Builder()\n // ...\n .build()\n\n onWasmReady {\n Window(\"Inbox\") {\n CircuitCompositionLocals(circuit) {\n // ...\n }\n }\n }\n}\n
"},{"location":"tutorial/#circuitcontent","title":"CircuitContent
","text":"CircuitContent
(docs) is a simple composable that takes a Screen
and renders it.
CircuitCompositionLocals(circuit) {\n CircuitContent(InboxScreen)\n}\n
Under the hood, this instantiates the corresponding Presenter
and Ui
from the local Circuit
instance and connects them together. All you need to do is pass in the Screen
you want to render!
This is the most basic way to render a Screen
. These can be top-level UIs or nested within other UIs. You can even have multiple CircuitContent
instances in the same composition.
An app architecture isn\u2019t complete without navigation. Circuit provides a simple navigation API that\u2019s focused around a simple BackStack
(docs) that is navigated via a Navigator
interface (docs). In most cases, you can use the built-in SaveableBackStack
implementation (docs), which is saved and restored in accordance with whatever the platform\u2019s rememberSaveable
implementation is.
val backStack = rememberSaveableBackStack(root = InboxScreen)\nval navigator = rememberCircuitNavigator(backStack) {\n // Do something when the root screen is popped, usually exiting the app\n}\n
Once you have these two components created, you can pass them to an advanced version of CircuitContent
that supports navigation called NavigableCircuitContent
(docs).
NavigableCircuitContent(navigator = navigator, backstack = backStack)\n
This composable will automatically manage the backstack and navigation for you, essentially rendering the \u201ctop\u201d of the back stack as your navigator navigates it. This also handles transitions between screens (NavDecoration
) and fallback behavior with Circuit.Builder.onUnavailableRoute
(docs).
Like with Circuit
, this is usually a one-time setup in your application at its primary entry point.
val backStack = rememberSaveableBackStack(root = InboxScreen)\nval navigator = rememberCircuitNavigator(backStack) {\n // Do something when the root screen is popped, usually exiting the app\n}\nCircuitCompositionLocals(circuit) {\n NavigableCircuitContent(navigator = navigator, backstack = backStack)\n}\n
"},{"location":"tutorial/#add-a-detail-screen","title":"Add a detail screen","text":"Detail Now that we have navigation set up, let\u2019s add a detail screen to our app to navigate to.
This screen will show the content of a specific email from the inbox, and in a real app would also show content like the chain history.
First, let\u2019s define a DetailScreen
and state.
@Parcelize\ndata class DetailScreen(val emailId: String) : Screen {\n data class State(val email: Email) : CircuitUiState\n}\n
DetailScreen.ktdata class DetailScreen(val emailId: String) : Screen {\n data class State(val email: Email) : CircuitUiState\n}\n
Notice that this time we use a data class
instead of a data object
. This is because we want to be able to pass in an emailId
to the screen. We\u2019ll use this to fetch the email from our data layer.
Warning
You should keep Screen
parameters as simple as possible and derive any additional data you need from your data layer instead.
Next, let\u2019s define a Presenter and UI for this screen.
PresenterUI DetailScreen.ktclass DetailPresenter(\n private val screen: DetailScreen,\n private val emailRepository: EmailRepository\n) : Presenter<DetailScreen.State> {\n @Composable\n override fun present(): DetailScreen.State {\n val email = emailRepository.getEmail(screen.emailId)\n return DetailScreen.State(email)\n }\n}\n
DetailScreen.kt@Composable\nfun EmailDetail(state: DetailScreen.State, modifier: Modifier = Modifier) {\n // ...\n // Write one or use EmailDetailContent from ui.kt\n}\n
Note that we\u2019re injecting the DetailScreen
into our Presenter
so we can get the email ID. This is where Circuit\u2019s factory pattern comes into play. Let\u2019s define a factory for our DetailPresenter
.
class DetailPresenter(...) : Presenter<DetailScreen.State> {\n // ...\n class Factory(private val emailRepository: EmailRepository) : Presenter.Factory {\n override fun create(screen: Screen, navigator: Navigator, context: CircuitContext): Presenter<*>? {\n return when (screen) {\n is DetailScreen -> return DetailPresenter(screen, emailRepository)\n else -> null\n }\n }\n }\n}\n
Here we have access to the screen and dynamically create the presenter we need. It can then pass the screen on to the presenter.
We can then wire these detail components to our Circuit
instance too.
val circuit: Circuit =\n Circuit.Builder()\n // ...\n .addPresenterFactory(DetailPresenter.Factory(emailRepository))\n .addUi<DetailScreen, DetailScreen.State> { state, modifier -> EmailDetail(state, modifier) }\n .build()\n
"},{"location":"tutorial/#navigate-to-the-detail-screen","title":"Navigate to the detail screen","text":"Now that we have a detail screen, let\u2019s navigate to it from our inbox list. As you can see in our presenter factory above, Circuit also offers access to a Navigator
in this create()
call that factories can then pass on to their created presenters.
Let\u2019s add a Navigator
property to our presenter and create a factory for our inbox screen now.
class InboxPresenter(\n private val navigator: Navigator,\n private val emailRepository: EmailRepository\n) : Presenter<InboxScreen.State> {\n // ...\n class Factory(private val emailRepository: EmailRepository) : Presenter.Factory {\n override fun create(screen: Screen, navigator: Navigator, context: CircuitContext): Presenter<*>? {\n return when (screen) {\n InboxScreen -> return InboxPresenter(navigator, emailRepository)\n else -> null\n }\n }\n }\n}\n
val circuit: Circuit =\n Circuit.Builder()\n .addPresenterFactory(InboxPresenter.Factory(emailRepository))\n .addUi<InboxScreen, InboxScreen.State> { state, modifier -> Inbox(state, modifier) }\n .addPresenterFactory(DetailPresenter.Factory(emailRepository))\n .addUi<DetailScreen, DetailScreen.State> { state, modifier -> EmailDetail(state, modifier) }\n .build()\n
Now that we have a Navigator
in our inbox presenter, we can use it to navigate to the detail screen. First, we need to explore how events work in Circuit.
So far, we\u2019ve covered state. State is produced by the presenter and consumed by the UI. That\u2019s only half of the UDF picture though! Events are the inverse: they\u2019re produced by the UI and consumed by the presenter. Events are how you can trigger actions in your app, such as navigation. This completes the circuit.
Events in Circuit are a little unconventional in that Circuit doesn\u2019t provide structured APIs for pipelining events from the UI to presenters. Instead, we use an event sink property pattern, where states contain a trailing eventSink
function that receives events emitted from the UI.
This provides many benefits, see the events guide for more information.
Let\u2019s add an event to our inbox screen for when the user clicks on an email.
Events must implement CircuitUiEvent
(docs) and are usually modeled as a sealed interface
hierarchy, where each subtype is a different event type.
data object InboxScreen : Screen {\n data class State(\n val emails: List<Email>,\n val eventSink: (Event) -> Unit\n ) : CircuitUiState\n sealed class Event : CircuitUiEvent {\n data class EmailClicked(val emailId: String) : Event()\n }\n}\n
Now that we have an event, let\u2019s emit it from our UI.
InboxScreen.kt@Composable\nfun Inbox(state: InboxScreen.State, modifier: Modifier = Modifier) {\n Scaffold(modifier = modifier, topBar = { TopAppBar(title = { Text(\"Inbox\") }) }) { innerPadding ->\n LazyColumn(modifier = Modifier.padding(innerPadding)) {\n items(state.emails) { email ->\n EmailItem(\n email = email,\n onClick = { state.eventSink(InboxScreen.Event.EmailClicked(email.id)) },\n )\n }\n }\n }\n}\n\n// Write one or use EmailItem from ui.kt\nprivate fun EmailItem(email: Email, modifier: Modifier = Modifier, onClick: () -> Unit) {\n // ...\n}\n
Finally, let\u2019s handle this event in our presenter.
InboxScreen.ktclass InboxPresenter(\n private val navigator: Navigator,\n private val emailRepository: EmailRepository\n) : Presenter<InboxScreen.State> {\n @Composable\n override fun present(): InboxScreen.State {\n // ...\n return InboxScreen.State(emails) { event ->\n when (event) {\n // Navigate to the detail screen when an email is clicked\n is EmailClicked -> navigator.goTo(DetailScreen(event.emailId))\n }\n }\n }\n}\n
This demonstrates how we can navigate forward in our app and pass data with it. Let\u2019s see how we can navigate back.
"},{"location":"tutorial/#navigating-back","title":"Navigating back","text":"Naturally, navigation can\u2019t be just one way. The opposite of Navigator.goTo()
is Navigator.pop()
, which pops the back stack back to the previous screen. To use this, let\u2019s add a back button to our detail screen and wire it up to a Navigator
.
data class DetailScreen(val emailId: String) : Screen {\n data class State(\n val email: Email,\n val eventSink: (Event) -> Unit\n ) : CircuitUiState\n sealed class Event : CircuitUiEvent {\n data object BackClicked : Event()\n }\n}\n
DetailScreen.kt@Composable\nfun EmailDetail(state: DetailScreen.State, modifier: Modifier = Modifier) {\n Scaffold(\n modifier = modifier,\n topBar = {\n TopAppBar(\n title = { Text(state.email.subject) },\n navigationIcon = {\n IconButton(onClick = { state.eventSink(DetailScreen.Event.BackClicked) }) {\n Icon(Icons.Default.ArrowBack, contentDescription = \"Back\")\n }\n },\n )\n },\n ) { innerPadding ->\n // Remaining detail UI...\n }\n}\n
DetailScreen.ktclass DetailPresenter(\n private val screen: DetailScreen,\n private val navigator: Navigator,\n private val emailRepository: EmailRepository,\n) : Presenter<DetailScreen.State> {\n @Composable\n override fun present(): DetailScreen.State {\n // ...\n return DetailScreen.State(email) { event ->\n when (event) {\n DetailScreen.Event.BackClicked -> navigator.pop()\n }\n }\n }\n // ...\n}\n
On Android, NavigableCircuitContent
automatically hooks into BackHandler to automatically pop on system back presses. On Desktop, it\u2019s recommended to wire the ESC key.
This is just a brief introduction to Circuit. For more information see various docs on the site, samples in the repo, the API reference, and check out other Circuit tools like circuit-retained, CircuitX, factory code gen, overlays, navigation with results, testing, multiplatform, and more.
"},{"location":"ui/","title":"UI","text":"The core Ui interface is simply this:
interface Ui<UiState : CircuitUiState> {\n @Composable fun Content(state: UiState, modifier: Modifier)\n}\n
Like presenters, simple UIs can also skip the class all together for use in other UIs. Core unit of granularity is just the @Composable function. In fact, when implementing these in practice they rarely use dependency injection at all and can normally just be written as top-level composable functions annotated with@CircuitInject
.
@CircuitInject<FavoritesScreen> // Relevant DI wiring is generated\n@Composable\nprivate fun Favorites(state: FavoritesState, modifier: Modifier = Modifier) {\n // ...\n}\n
Writing UIs like this has a number of benefits.
Let\u2019s look a little more closely at the last bullet point about preview functions. With the above example, we can easily stand up previews for all of our different states!
@Preview\n@Composable\nprivate fun PreviewFavorites() = Favorites(FavoritesState(listOf(\"Reeses\", \"Lola\")))\n\n@Preview\n@Composable\nprivate fun PreviewEmptyFavorites() = Favorites(FavoritesState(listOf()))\n
"},{"location":"ui/#static-ui","title":"Static UI","text":"In some cases, a UI may not need a presenter to compute or manage its state. Examples of this include UIs that are stateless or can derive their state from a single static input or an input [Screen]\u2019s properties. In these cases, make your screen implement the StaticScreen
interface. When a StaticScreen
is used, Circuit will internally allow the UI to run on its own and won\u2019t connect it to a presenter if no presenter is provided.