diff --git a/docs/shared-elements-tutorial.md b/docs/shared-elements-tutorial.md
index 7cee85cd3..f52f1732f 100644
--- a/docs/shared-elements-tutorial.md
+++ b/docs/shared-elements-tutorial.md
@@ -176,7 +176,15 @@ The call to `requireAnimatedScope` is accessing a `AnimatedVisibilityScope` that
=== "Android"
-
+
+
+ With that we now have a shared element transition where the sender image transitions across the two screens!
+
+
+
+=== "Desktop"
+
+
With that we now have a shared element transition where the sender image transitions across the two screens!
@@ -244,7 +252,16 @@ Text(
=== "Android"
-
+
+
+ After the `Modifier.sharedBounds()` is added to each of the three `Text` in the `EmailItem` composable and the `EmailDetailContent` composable you should now see the majority of the email tranistioning across the two `Screens`.
+
+
+
+=== "Desktop"
+
+
+
After the `Modifier.sharedBounds()` is added to each of the three `Text` in the `EmailItem` composable and the `EmailDetailContent` composable you should now see the majority of the email tranistioning across the two `Screens`.
diff --git a/docs/tutorial.md b/docs/tutorial.md
index 5e80762f9..296d14d3e 100644
--- a/docs/tutorial.md
+++ b/docs/tutorial.md
@@ -570,6 +570,10 @@ Naturally, navigation can't be just one way. The opposite of `Navigator.goTo()`
On Android, `NavigableCircuitContent` automatically hooks into [BackHandler](https://developer.android.com/reference/kotlin/androidx/activity/compose/package-summary#BackHandler(kotlin.Boolean,kotlin.Function0)) to automatically pop on system back presses. On Desktop, it's recommended to wire the ESC key.
+## Shared Elements
+
+You can continue this tutorial by seting up [shared element transitions](shared-elements-tutorial.md) between the Inbox and Detail screens.
+
## Conclusion
This is just a brief introduction to Circuit. For more information see various docs on the site, samples in the repo, the [API reference](api/0.x/index.html), and check out other Circuit tools like [circuit-retained](https://slackhq.github.io/circuit/presenter/#retention), [CircuitX](https://slackhq.github.io/circuit/circuitx/), [factory code gen](https://slackhq.github.io/circuit/code-gen/), [overlays](https://slackhq.github.io/circuit/overlays/), [navigation with results](https://slackhq.github.io/circuit/navigation/#results), [testing](https://slackhq.github.io/circuit/testing/), [multiplatform](https://slackhq.github.io/circuit/setup/#platform-support), and more.
diff --git a/docs/videos/shared-elements-tutorial-step-4.mp4 b/docs/videos/shared-elements-tutorial-step-4-android.mp4
similarity index 100%
rename from docs/videos/shared-elements-tutorial-step-4.mp4
rename to docs/videos/shared-elements-tutorial-step-4-android.mp4
diff --git a/docs/videos/shared-elements-tutorial-step-4-desktop.mp4 b/docs/videos/shared-elements-tutorial-step-4-desktop.mp4
new file mode 100644
index 000000000..02482634c
Binary files /dev/null and b/docs/videos/shared-elements-tutorial-step-4-desktop.mp4 differ
diff --git a/docs/videos/shared-elements-tutorial-step-5.mp4 b/docs/videos/shared-elements-tutorial-step-5-android.mp4
similarity index 100%
rename from docs/videos/shared-elements-tutorial-step-5.mp4
rename to docs/videos/shared-elements-tutorial-step-5-android.mp4
diff --git a/docs/videos/shared-elements-tutorial-step-5-desktop.mp4 b/docs/videos/shared-elements-tutorial-step-5-desktop.mp4
new file mode 100644
index 000000000..604965b2c
Binary files /dev/null and b/docs/videos/shared-elements-tutorial-step-5-desktop.mp4 differ
diff --git a/mkdocs.yml b/mkdocs.yml
index 19d92a6bd..4110d40c2 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -72,7 +72,7 @@ nav:
- 'Ui': ui.md
- 'Overlays': overlays.md
- 'Shared Elements':
- 'Documentation': shared-elements.md
+ 'Usage': shared-elements.md
'Tutorial': shared-elements-tutorial.md
- 'Testing': testing.md
- 'Factories': factories.md
diff --git a/samples/tutorial/src/androidMain/kotlin/com/slack/circuit/tutorial/MainActivity.kt b/samples/tutorial/src/androidMain/kotlin/com/slack/circuit/tutorial/MainActivity.kt
index 663c865c0..035bf06c4 100644
--- a/samples/tutorial/src/androidMain/kotlin/com/slack/circuit/tutorial/MainActivity.kt
+++ b/samples/tutorial/src/androidMain/kotlin/com/slack/circuit/tutorial/MainActivity.kt
@@ -6,7 +6,7 @@ import android.os.Bundle
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
-import com.slack.circuit.tutorial.impl.tutorialOnCreate
+import com.slack.circuit.tutorial.intro.introTutorialOnCreate
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@@ -18,6 +18,7 @@ class MainActivity : AppCompatActivity() {
?.isAppearanceLightStatusBars = true
// TODO replace with your own impl if following the tutorial!
- tutorialOnCreate()
+ introTutorialOnCreate()
+ // sharedElementsTutorialOnCreate()
}
}
diff --git a/samples/tutorial/src/androidMain/kotlin/com/slack/circuit/tutorial/impl/DetailScreen.kt b/samples/tutorial/src/androidMain/kotlin/com/slack/circuit/tutorial/intro/DetailScreen.kt
similarity index 96%
rename from samples/tutorial/src/androidMain/kotlin/com/slack/circuit/tutorial/impl/DetailScreen.kt
rename to samples/tutorial/src/androidMain/kotlin/com/slack/circuit/tutorial/intro/DetailScreen.kt
index 16432fb15..3db2bb893 100644
--- a/samples/tutorial/src/androidMain/kotlin/com/slack/circuit/tutorial/impl/DetailScreen.kt
+++ b/samples/tutorial/src/androidMain/kotlin/com/slack/circuit/tutorial/intro/DetailScreen.kt
@@ -1,6 +1,6 @@
// Copyright (C) 2024 Slack Technologies, LLC
// SPDX-License-Identifier: Apache-2.0
-package com.slack.circuit.tutorial.impl
+package com.slack.circuit.tutorial.intro
import androidx.compose.foundation.layout.Arrangement.spacedBy
import androidx.compose.foundation.layout.Column
@@ -25,8 +25,8 @@ import com.slack.circuit.runtime.Navigator
import com.slack.circuit.runtime.presenter.Presenter
import com.slack.circuit.runtime.screen.Screen
import com.slack.circuit.tutorial.common.Email
-import com.slack.circuit.tutorial.common.EmailDetailContent
import com.slack.circuit.tutorial.common.EmailRepository
+import com.slack.circuit.tutorial.common.intro.EmailDetailContent
import kotlinx.parcelize.Parcelize
@Parcelize
diff --git a/samples/tutorial/src/androidMain/kotlin/com/slack/circuit/tutorial/impl/InboxScreen.kt b/samples/tutorial/src/androidMain/kotlin/com/slack/circuit/tutorial/intro/InboxScreen.kt
similarity index 96%
rename from samples/tutorial/src/androidMain/kotlin/com/slack/circuit/tutorial/impl/InboxScreen.kt
rename to samples/tutorial/src/androidMain/kotlin/com/slack/circuit/tutorial/intro/InboxScreen.kt
index 1ea661cf7..4b23f8337 100644
--- a/samples/tutorial/src/androidMain/kotlin/com/slack/circuit/tutorial/impl/InboxScreen.kt
+++ b/samples/tutorial/src/androidMain/kotlin/com/slack/circuit/tutorial/intro/InboxScreen.kt
@@ -1,6 +1,6 @@
// Copyright (C) 2024 Slack Technologies, LLC
// SPDX-License-Identifier: Apache-2.0
-package com.slack.circuit.tutorial.impl
+package com.slack.circuit.tutorial.intro
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
@@ -19,8 +19,8 @@ import com.slack.circuit.runtime.Navigator
import com.slack.circuit.runtime.presenter.Presenter
import com.slack.circuit.runtime.screen.Screen
import com.slack.circuit.tutorial.common.Email
-import com.slack.circuit.tutorial.common.EmailItem
import com.slack.circuit.tutorial.common.EmailRepository
+import com.slack.circuit.tutorial.common.intro.EmailItem
import kotlinx.parcelize.Parcelize
@Parcelize
diff --git a/samples/tutorial/src/androidMain/kotlin/com/slack/circuit/tutorial/impl/MainActivityImpl.kt b/samples/tutorial/src/androidMain/kotlin/com/slack/circuit/tutorial/intro/MainActivityImpl.kt
similarity index 93%
rename from samples/tutorial/src/androidMain/kotlin/com/slack/circuit/tutorial/impl/MainActivityImpl.kt
rename to samples/tutorial/src/androidMain/kotlin/com/slack/circuit/tutorial/intro/MainActivityImpl.kt
index 1cb46e2b8..6dc1862e8 100644
--- a/samples/tutorial/src/androidMain/kotlin/com/slack/circuit/tutorial/impl/MainActivityImpl.kt
+++ b/samples/tutorial/src/androidMain/kotlin/com/slack/circuit/tutorial/intro/MainActivityImpl.kt
@@ -1,6 +1,6 @@
// Copyright (C) 2024 Slack Technologies, LLC
// SPDX-License-Identifier: Apache-2.0
-package com.slack.circuit.tutorial.impl
+package com.slack.circuit.tutorial.intro
import androidx.activity.compose.setContent
import androidx.compose.material3.MaterialTheme
@@ -12,7 +12,7 @@ import com.slack.circuit.foundation.rememberCircuitNavigator
import com.slack.circuit.tutorial.MainActivity
import com.slack.circuit.tutorial.common.EmailRepository
-fun MainActivity.tutorialOnCreate() {
+fun MainActivity.introTutorialOnCreate() {
val emailRepository = EmailRepository()
val circuit: Circuit =
Circuit.Builder()
diff --git a/samples/tutorial/src/androidMain/kotlin/com/slack/circuit/tutorial/sharedelements/DetailScreen.kt b/samples/tutorial/src/androidMain/kotlin/com/slack/circuit/tutorial/sharedelements/DetailScreen.kt
new file mode 100644
index 000000000..31e3335a9
--- /dev/null
+++ b/samples/tutorial/src/androidMain/kotlin/com/slack/circuit/tutorial/sharedelements/DetailScreen.kt
@@ -0,0 +1,90 @@
+// Copyright (C) 2024 Slack Technologies, LLC
+// SPDX-License-Identifier: Apache-2.0
+package com.slack.circuit.tutorial.sharedelements
+
+import androidx.compose.foundation.layout.Arrangement.spacedBy
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.slack.circuit.runtime.CircuitContext
+import com.slack.circuit.runtime.CircuitUiEvent
+import com.slack.circuit.runtime.CircuitUiState
+import com.slack.circuit.runtime.Navigator
+import com.slack.circuit.runtime.presenter.Presenter
+import com.slack.circuit.runtime.screen.Screen
+import com.slack.circuit.tutorial.common.Email
+import com.slack.circuit.tutorial.common.EmailRepository
+import com.slack.circuit.tutorial.common.sharedelements.EmailDetailContent
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class DetailScreen(val emailId: String) : Screen {
+ data class State(val email: Email, val eventSink: (Event) -> Unit) : CircuitUiState
+
+ sealed interface Event : CircuitUiEvent {
+ data object BackClicked : Event
+ }
+}
+
+class DetailPresenter(
+ private val screen: DetailScreen,
+ private val navigator: Navigator,
+ private val emailRepository: EmailRepository,
+) : Presenter {
+ @Composable
+ override fun present(): DetailScreen.State {
+ val email = emailRepository.getEmail(screen.emailId)
+ return DetailScreen.State(email) { event ->
+ when (event) {
+ DetailScreen.Event.BackClicked -> navigator.pop()
+ }
+ }
+ }
+
+ class Factory(private val emailRepository: EmailRepository) : Presenter.Factory {
+ override fun create(
+ screen: Screen,
+ navigator: Navigator,
+ context: CircuitContext,
+ ): Presenter<*>? {
+ return when (screen) {
+ is DetailScreen -> return DetailPresenter(screen, navigator, emailRepository)
+ else -> null
+ }
+ }
+ }
+}
+
+@Composable
+fun EmailDetail(state: DetailScreen.State, modifier: Modifier = Modifier) {
+ val subject by remember { derivedStateOf { state.email.subject } }
+ Scaffold(
+ modifier = modifier,
+ topBar = {
+ TopAppBar(
+ title = { Text(subject) },
+ navigationIcon = {
+ IconButton(onClick = { state.eventSink(DetailScreen.Event.BackClicked) }) {
+ Icon(Icons.AutoMirrored.Default.ArrowBack, contentDescription = "Back")
+ }
+ },
+ )
+ },
+ ) { innerPadding ->
+ Column(modifier = Modifier.padding(innerPadding), verticalArrangement = spacedBy(16.dp)) {
+ EmailDetailContent(state.email)
+ }
+ }
+}
diff --git a/samples/tutorial/src/androidMain/kotlin/com/slack/circuit/tutorial/sharedelements/InboxScreen.kt b/samples/tutorial/src/androidMain/kotlin/com/slack/circuit/tutorial/sharedelements/InboxScreen.kt
new file mode 100644
index 000000000..7d3dcfda6
--- /dev/null
+++ b/samples/tutorial/src/androidMain/kotlin/com/slack/circuit/tutorial/sharedelements/InboxScreen.kt
@@ -0,0 +1,76 @@
+// Copyright (C) 2024 Slack Technologies, LLC
+// SPDX-License-Identifier: Apache-2.0
+package com.slack.circuit.tutorial.sharedelements
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.produceState
+import androidx.compose.ui.Modifier
+import com.slack.circuit.runtime.CircuitContext
+import com.slack.circuit.runtime.CircuitUiEvent
+import com.slack.circuit.runtime.CircuitUiState
+import com.slack.circuit.runtime.Navigator
+import com.slack.circuit.runtime.presenter.Presenter
+import com.slack.circuit.runtime.screen.Screen
+import com.slack.circuit.tutorial.common.Email
+import com.slack.circuit.tutorial.common.EmailRepository
+import com.slack.circuit.tutorial.common.sharedelements.EmailItem
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data object InboxScreen : Screen {
+ data class State(val emails: List, val eventSink: (Event) -> Unit) : CircuitUiState
+
+ sealed class Event : CircuitUiEvent {
+ data class EmailClicked(val emailId: String) : Event()
+ }
+}
+
+class InboxPresenter(
+ private val navigator: Navigator,
+ private val emailRepository: EmailRepository,
+) : Presenter {
+ @Composable
+ override fun present(): InboxScreen.State {
+ val emails by
+ produceState>(initialValue = emptyList()) { value = emailRepository.getEmails() }
+ return InboxScreen.State(emails) { event ->
+ when (event) {
+ is InboxScreen.Event.EmailClicked -> navigator.goTo(DetailScreen(event.emailId))
+ }
+ }
+ }
+
+ class Factory(private val emailRepository: EmailRepository) : Presenter.Factory {
+ override fun create(
+ screen: Screen,
+ navigator: Navigator,
+ context: CircuitContext,
+ ): Presenter<*>? {
+ return when (screen) {
+ InboxScreen -> return InboxPresenter(navigator, emailRepository)
+ else -> null
+ }
+ }
+ }
+}
+
+@Composable
+fun Inbox(state: InboxScreen.State, modifier: Modifier = Modifier) {
+ Scaffold(modifier = modifier, topBar = { TopAppBar(title = { Text("Inbox") }) }) { innerPadding ->
+ LazyColumn(modifier = Modifier.padding(innerPadding)) {
+ items(state.emails) { email ->
+ EmailItem(
+ email = email,
+ onClick = { state.eventSink(InboxScreen.Event.EmailClicked(email.id)) },
+ )
+ }
+ }
+ }
+}
diff --git a/samples/tutorial/src/androidMain/kotlin/com/slack/circuit/tutorial/sharedelements/MainActivityImpl.kt b/samples/tutorial/src/androidMain/kotlin/com/slack/circuit/tutorial/sharedelements/MainActivityImpl.kt
new file mode 100644
index 000000000..2872098ab
--- /dev/null
+++ b/samples/tutorial/src/androidMain/kotlin/com/slack/circuit/tutorial/sharedelements/MainActivityImpl.kt
@@ -0,0 +1,38 @@
+// Copyright (C) 2024 Slack Technologies, LLC
+// SPDX-License-Identifier: Apache-2.0
+package com.slack.circuit.tutorial.sharedelements
+
+import androidx.activity.compose.setContent
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.material3.MaterialTheme
+import com.slack.circuit.backstack.rememberSaveableBackStack
+import com.slack.circuit.foundation.Circuit
+import com.slack.circuit.foundation.CircuitCompositionLocals
+import com.slack.circuit.foundation.NavigableCircuitContent
+import com.slack.circuit.foundation.rememberCircuitNavigator
+import com.slack.circuit.sharedelements.SharedElementTransitionLayout
+import com.slack.circuit.tutorial.MainActivity
+import com.slack.circuit.tutorial.common.EmailRepository
+
+@OptIn(ExperimentalSharedTransitionApi::class)
+fun MainActivity.sharedElementsTutorialOnCreate() {
+ val emailRepository = EmailRepository()
+ val circuit: Circuit =
+ Circuit.Builder()
+ .addPresenterFactory(DetailPresenter.Factory(emailRepository))
+ .addPresenterFactory(InboxPresenter.Factory(emailRepository))
+ .addUi { state, modifier -> Inbox(state, modifier) }
+ .addUi { state, modifier -> EmailDetail(state, modifier) }
+ .build()
+ setContent {
+ MaterialTheme {
+ val backStack = rememberSaveableBackStack(InboxScreen)
+ val navigator = rememberCircuitNavigator(backStack)
+ CircuitCompositionLocals(circuit) {
+ SharedElementTransitionLayout {
+ NavigableCircuitContent(navigator = navigator, backStack = backStack)
+ }
+ }
+ }
+ }
+}
diff --git a/samples/tutorial/src/commonMain/kotlin/com/slack/circuit/tutorial/common/ui.kt b/samples/tutorial/src/commonMain/kotlin/com/slack/circuit/tutorial/common/intro/ui.kt
similarity index 97%
rename from samples/tutorial/src/commonMain/kotlin/com/slack/circuit/tutorial/common/ui.kt
rename to samples/tutorial/src/commonMain/kotlin/com/slack/circuit/tutorial/common/intro/ui.kt
index bfb124cdd..8dccd5d61 100644
--- a/samples/tutorial/src/commonMain/kotlin/com/slack/circuit/tutorial/common/ui.kt
+++ b/samples/tutorial/src/commonMain/kotlin/com/slack/circuit/tutorial/common/intro/ui.kt
@@ -1,6 +1,6 @@
// Copyright (C) 2024 Slack Technologies, LLC
// SPDX-License-Identifier: Apache-2.0
-package com.slack.circuit.tutorial.common
+package com.slack.circuit.tutorial.common.intro
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@@ -25,6 +25,7 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
+import com.slack.circuit.tutorial.common.Email
/** A simple email item to show in a list. */
@Composable
diff --git a/samples/tutorial/src/commonMain/kotlin/com/slack/circuit/tutorial/common/sharedelements/EmailSharedTransitionKey.kt b/samples/tutorial/src/commonMain/kotlin/com/slack/circuit/tutorial/common/sharedelements/EmailSharedTransitionKey.kt
new file mode 100644
index 000000000..3d4f8a8c6
--- /dev/null
+++ b/samples/tutorial/src/commonMain/kotlin/com/slack/circuit/tutorial/common/sharedelements/EmailSharedTransitionKey.kt
@@ -0,0 +1,14 @@
+// Copyright (C) 2025 Slack Technologies, LLC
+// SPDX-License-Identifier: Apache-2.0
+package com.slack.circuit.tutorial.common.sharedelements
+
+import com.slack.circuit.sharedelements.SharedTransitionKey
+
+data class EmailSharedTransitionKey(val id: String, val type: ElementType) : SharedTransitionKey {
+ enum class ElementType {
+ SenderImage,
+ SenderName,
+ Subject,
+ Body,
+ }
+}
diff --git a/samples/tutorial/src/commonMain/kotlin/com/slack/circuit/tutorial/common/sharedelements/ui.kt b/samples/tutorial/src/commonMain/kotlin/com/slack/circuit/tutorial/common/sharedelements/ui.kt
new file mode 100644
index 000000000..42f0748cf
--- /dev/null
+++ b/samples/tutorial/src/commonMain/kotlin/com/slack/circuit/tutorial/common/sharedelements/ui.kt
@@ -0,0 +1,225 @@
+// Copyright (C) 2024 Slack Technologies, LLC
+// SPDX-License-Identifier: Apache-2.0
+package com.slack.circuit.tutorial.common.sharedelements
+
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Person
+import androidx.compose.material3.Divider
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import com.slack.circuit.sharedelements.SharedElementTransitionScope
+import com.slack.circuit.tutorial.common.Email
+
+/** A simple email item to show in a list. */
+@OptIn(ExperimentalSharedTransitionApi::class)
+@Composable
+fun EmailItem(email: Email, modifier: Modifier = Modifier, onClick: () -> Unit = {}) =
+ SharedElementTransitionScope {
+ Row(
+ modifier.clickable(onClick = onClick).padding(16.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ Image(
+ Icons.Default.Person,
+ modifier =
+ Modifier.sharedElement(
+ state =
+ rememberSharedContentState(
+ EmailSharedTransitionKey(
+ id = email.id,
+ type = EmailSharedTransitionKey.ElementType.SenderImage,
+ )
+ ),
+ animatedVisibilityScope =
+ requireAnimatedScope(SharedElementTransitionScope.AnimatedScope.Navigation),
+ )
+ .size(40.dp)
+ .clip(CircleShape)
+ .background(Color.Magenta)
+ .padding(4.dp),
+ colorFilter = ColorFilter.tint(Color.White),
+ contentDescription = null,
+ )
+ Column {
+ Row {
+ Text(
+ text = email.sender,
+ modifier =
+ Modifier.sharedBounds(
+ sharedContentState =
+ rememberSharedContentState(
+ EmailSharedTransitionKey(
+ id = email.id,
+ type = EmailSharedTransitionKey.ElementType.SenderName,
+ )
+ ),
+ animatedVisibilityScope =
+ requireAnimatedScope(SharedElementTransitionScope.AnimatedScope.Navigation),
+ )
+ .weight(1f),
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold,
+ )
+
+ Text(
+ text = email.timestamp,
+ style = MaterialTheme.typography.bodySmall,
+ modifier = Modifier.alpha(0.5f),
+ )
+ }
+
+ Text(
+ text = email.subject,
+ modifier =
+ Modifier.sharedBounds(
+ sharedContentState =
+ rememberSharedContentState(
+ EmailSharedTransitionKey(
+ id = email.id,
+ type = EmailSharedTransitionKey.ElementType.Subject,
+ )
+ ),
+ animatedVisibilityScope =
+ requireAnimatedScope(SharedElementTransitionScope.AnimatedScope.Navigation),
+ ),
+ style = MaterialTheme.typography.labelLarge,
+ )
+ Text(
+ text = email.body,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ style = MaterialTheme.typography.bodyMedium,
+ modifier =
+ Modifier.sharedBounds(
+ sharedContentState =
+ rememberSharedContentState(
+ EmailSharedTransitionKey(
+ id = email.id,
+ type = EmailSharedTransitionKey.ElementType.Body,
+ )
+ ),
+ animatedVisibilityScope =
+ requireAnimatedScope(SharedElementTransitionScope.AnimatedScope.Navigation),
+ )
+ .alpha(0.5f),
+ )
+ }
+ }
+ }
+
+@OptIn(ExperimentalSharedTransitionApi::class)
+@Composable
+fun EmailDetailContent(email: Email, modifier: Modifier = Modifier) = SharedElementTransitionScope {
+ Column(modifier.padding(16.dp)) {
+ Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
+ Image(
+ Icons.Default.Person,
+ modifier =
+ Modifier.sharedElement(
+ state =
+ rememberSharedContentState(
+ EmailSharedTransitionKey(
+ id = email.id,
+ type = EmailSharedTransitionKey.ElementType.SenderImage,
+ )
+ ),
+ animatedVisibilityScope =
+ requireAnimatedScope(SharedElementTransitionScope.AnimatedScope.Navigation),
+ )
+ .size(40.dp)
+ .clip(CircleShape)
+ .background(Color.Magenta)
+ .padding(4.dp),
+ colorFilter = ColorFilter.tint(Color.White),
+ contentDescription = null,
+ )
+ Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
+ Row {
+ Text(
+ text = email.sender,
+ modifier =
+ Modifier.sharedBounds(
+ sharedContentState =
+ rememberSharedContentState(
+ EmailSharedTransitionKey(
+ id = email.id,
+ type = EmailSharedTransitionKey.ElementType.SenderName,
+ )
+ ),
+ animatedVisibilityScope =
+ requireAnimatedScope(SharedElementTransitionScope.AnimatedScope.Navigation),
+ )
+ .weight(1f),
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold,
+ )
+ Text(
+ text = email.timestamp,
+ style = MaterialTheme.typography.bodySmall,
+ modifier = Modifier.alpha(0.5f),
+ )
+ }
+ Text(
+ text = email.subject,
+ modifier =
+ Modifier.sharedBounds(
+ sharedContentState =
+ rememberSharedContentState(
+ EmailSharedTransitionKey(
+ id = email.id,
+ type = EmailSharedTransitionKey.ElementType.Subject,
+ )
+ ),
+ animatedVisibilityScope =
+ requireAnimatedScope(SharedElementTransitionScope.AnimatedScope.Navigation),
+ ),
+ style = MaterialTheme.typography.labelMedium,
+ )
+ Row {
+ Text("To: ", style = MaterialTheme.typography.labelMedium, fontWeight = FontWeight.Bold)
+ Text(
+ text = email.recipients.joinToString(","),
+ style = MaterialTheme.typography.labelMedium,
+ modifier = Modifier.alpha(0.5f),
+ )
+ }
+ }
+ }
+ @Suppress("DEPRECATION") // Deprecated in Android but not yet available in CM
+ Divider(modifier = Modifier.padding(vertical = 16.dp))
+ Text(
+ text = email.body,
+ Modifier.sharedBounds(
+ sharedContentState =
+ rememberSharedContentState(
+ EmailSharedTransitionKey(
+ id = email.id,
+ type = EmailSharedTransitionKey.ElementType.Body,
+ )
+ ),
+ animatedVisibilityScope =
+ requireAnimatedScope(SharedElementTransitionScope.AnimatedScope.Navigation),
+ ),
+ style = MaterialTheme.typography.bodyMedium,
+ )
+ }
+}
diff --git a/samples/tutorial/src/jvmMain/kotlin/com/slack/circuit/tutorial/impl/DetailScreen.kt b/samples/tutorial/src/jvmMain/kotlin/com/slack/circuit/tutorial/intro/DetailScreen.kt
similarity index 96%
rename from samples/tutorial/src/jvmMain/kotlin/com/slack/circuit/tutorial/impl/DetailScreen.kt
rename to samples/tutorial/src/jvmMain/kotlin/com/slack/circuit/tutorial/intro/DetailScreen.kt
index fadf7df61..aeb60c591 100644
--- a/samples/tutorial/src/jvmMain/kotlin/com/slack/circuit/tutorial/impl/DetailScreen.kt
+++ b/samples/tutorial/src/jvmMain/kotlin/com/slack/circuit/tutorial/intro/DetailScreen.kt
@@ -1,6 +1,6 @@
// Copyright (C) 2024 Slack Technologies, LLC
// SPDX-License-Identifier: Apache-2.0
-package com.slack.circuit.tutorial.impl
+package com.slack.circuit.tutorial.intro
import androidx.compose.foundation.layout.Arrangement.spacedBy
import androidx.compose.foundation.layout.Column
@@ -22,8 +22,8 @@ import com.slack.circuit.runtime.Navigator
import com.slack.circuit.runtime.presenter.Presenter
import com.slack.circuit.runtime.screen.Screen
import com.slack.circuit.tutorial.common.Email
-import com.slack.circuit.tutorial.common.EmailDetailContent
import com.slack.circuit.tutorial.common.EmailRepository
+import com.slack.circuit.tutorial.common.intro.EmailDetailContent
data class DetailScreen(val emailId: String) : Screen {
data class State(val email: Email, val eventSink: (Event) -> Unit) : CircuitUiState
diff --git a/samples/tutorial/src/jvmMain/kotlin/com/slack/circuit/tutorial/impl/InboxScreen.kt b/samples/tutorial/src/jvmMain/kotlin/com/slack/circuit/tutorial/intro/InboxScreen.kt
similarity index 96%
rename from samples/tutorial/src/jvmMain/kotlin/com/slack/circuit/tutorial/impl/InboxScreen.kt
rename to samples/tutorial/src/jvmMain/kotlin/com/slack/circuit/tutorial/intro/InboxScreen.kt
index e1b8a097c..51e7827c0 100644
--- a/samples/tutorial/src/jvmMain/kotlin/com/slack/circuit/tutorial/impl/InboxScreen.kt
+++ b/samples/tutorial/src/jvmMain/kotlin/com/slack/circuit/tutorial/intro/InboxScreen.kt
@@ -1,6 +1,6 @@
// Copyright (C) 2024 Slack Technologies, LLC
// SPDX-License-Identifier: Apache-2.0
-package com.slack.circuit.tutorial.impl
+package com.slack.circuit.tutorial.intro
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
@@ -19,8 +19,8 @@ import com.slack.circuit.runtime.Navigator
import com.slack.circuit.runtime.presenter.Presenter
import com.slack.circuit.runtime.screen.Screen
import com.slack.circuit.tutorial.common.Email
-import com.slack.circuit.tutorial.common.EmailItem
import com.slack.circuit.tutorial.common.EmailRepository
+import com.slack.circuit.tutorial.common.intro.EmailItem
data object InboxScreen : Screen {
data class State(val emails: List, val eventSink: (Event) -> Unit) : CircuitUiState
diff --git a/samples/tutorial/src/jvmMain/kotlin/com/slack/circuit/tutorial/impl/main.kt b/samples/tutorial/src/jvmMain/kotlin/com/slack/circuit/tutorial/intro/main.kt
similarity index 97%
rename from samples/tutorial/src/jvmMain/kotlin/com/slack/circuit/tutorial/impl/main.kt
rename to samples/tutorial/src/jvmMain/kotlin/com/slack/circuit/tutorial/intro/main.kt
index d41a13b1b..66e3010dd 100644
--- a/samples/tutorial/src/jvmMain/kotlin/com/slack/circuit/tutorial/impl/main.kt
+++ b/samples/tutorial/src/jvmMain/kotlin/com/slack/circuit/tutorial/intro/main.kt
@@ -1,6 +1,6 @@
// Copyright (C) 2024 Slack Technologies, LLC
// SPDX-License-Identifier: Apache-2.0
-package com.slack.circuit.tutorial.impl
+package com.slack.circuit.tutorial.intro
import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.window.Window
diff --git a/samples/tutorial/src/jvmMain/kotlin/com/slack/circuit/tutorial/main.kt b/samples/tutorial/src/jvmMain/kotlin/com/slack/circuit/tutorial/main.kt
index 874847307..d0f504ef3 100644
--- a/samples/tutorial/src/jvmMain/kotlin/com/slack/circuit/tutorial/main.kt
+++ b/samples/tutorial/src/jvmMain/kotlin/com/slack/circuit/tutorial/main.kt
@@ -4,5 +4,6 @@ package com.slack.circuit.tutorial
fun main() {
// TODO replace with your own impl if following the tutorial!
- com.slack.circuit.tutorial.impl.main()
+ com.slack.circuit.tutorial.intro.main()
+ // com.slack.circuit.tutorial.sharedelements.main()
}
diff --git a/samples/tutorial/src/jvmMain/kotlin/com/slack/circuit/tutorial/sharedelements/DetailScreen.kt b/samples/tutorial/src/jvmMain/kotlin/com/slack/circuit/tutorial/sharedelements/DetailScreen.kt
new file mode 100644
index 000000000..ec9a21f15
--- /dev/null
+++ b/samples/tutorial/src/jvmMain/kotlin/com/slack/circuit/tutorial/sharedelements/DetailScreen.kt
@@ -0,0 +1,84 @@
+// Copyright (C) 2024 Slack Technologies, LLC
+// SPDX-License-Identifier: Apache-2.0
+package com.slack.circuit.tutorial.sharedelements
+
+import androidx.compose.foundation.layout.Arrangement.spacedBy
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import com.slack.circuit.runtime.CircuitContext
+import com.slack.circuit.runtime.CircuitUiEvent
+import com.slack.circuit.runtime.CircuitUiState
+import com.slack.circuit.runtime.Navigator
+import com.slack.circuit.runtime.presenter.Presenter
+import com.slack.circuit.runtime.screen.Screen
+import com.slack.circuit.tutorial.common.Email
+import com.slack.circuit.tutorial.common.EmailRepository
+import com.slack.circuit.tutorial.common.sharedelements.EmailDetailContent
+
+data class DetailScreen(val emailId: String) : Screen {
+ data class State(val email: Email, val eventSink: (Event) -> Unit) : CircuitUiState
+
+ sealed interface Event : CircuitUiEvent {
+ data object BackClicked : Event
+ }
+}
+
+class DetailPresenter(
+ private val screen: DetailScreen,
+ private val navigator: Navigator,
+ private val emailRepository: EmailRepository,
+) : Presenter {
+ @Composable
+ override fun present(): DetailScreen.State {
+ val email = emailRepository.getEmail(screen.emailId)
+ return DetailScreen.State(email) { event ->
+ when (event) {
+ DetailScreen.Event.BackClicked -> navigator.pop()
+ }
+ }
+ }
+
+ class Factory(private val emailRepository: EmailRepository) : Presenter.Factory {
+ override fun create(
+ screen: Screen,
+ navigator: Navigator,
+ context: CircuitContext,
+ ): Presenter<*>? {
+ return when (screen) {
+ is DetailScreen -> return DetailPresenter(screen, navigator, emailRepository)
+ else -> null
+ }
+ }
+ }
+}
+
+@Composable
+fun EmailDetail(state: DetailScreen.State, modifier: Modifier = Modifier) {
+ Scaffold(
+ modifier = modifier,
+ topBar = {
+ TopAppBar(
+ title = { Text(state.email.subject) },
+ navigationIcon = {
+ IconButton(onClick = { state.eventSink(DetailScreen.Event.BackClicked) }) {
+ Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
+ }
+ },
+ )
+ },
+ ) { innerPadding ->
+ Column(modifier = Modifier.padding(innerPadding), verticalArrangement = spacedBy(16.dp)) {
+ EmailDetailContent(state.email)
+ }
+ }
+}
diff --git a/samples/tutorial/src/jvmMain/kotlin/com/slack/circuit/tutorial/sharedelements/InboxScreen.kt b/samples/tutorial/src/jvmMain/kotlin/com/slack/circuit/tutorial/sharedelements/InboxScreen.kt
new file mode 100644
index 000000000..e00f31c57
--- /dev/null
+++ b/samples/tutorial/src/jvmMain/kotlin/com/slack/circuit/tutorial/sharedelements/InboxScreen.kt
@@ -0,0 +1,74 @@
+// Copyright (C) 2024 Slack Technologies, LLC
+// SPDX-License-Identifier: Apache-2.0
+package com.slack.circuit.tutorial.sharedelements
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.produceState
+import androidx.compose.ui.Modifier
+import com.slack.circuit.runtime.CircuitContext
+import com.slack.circuit.runtime.CircuitUiEvent
+import com.slack.circuit.runtime.CircuitUiState
+import com.slack.circuit.runtime.Navigator
+import com.slack.circuit.runtime.presenter.Presenter
+import com.slack.circuit.runtime.screen.Screen
+import com.slack.circuit.tutorial.common.Email
+import com.slack.circuit.tutorial.common.EmailRepository
+import com.slack.circuit.tutorial.common.sharedelements.EmailItem
+
+data object InboxScreen : Screen {
+ data class State(val emails: List, val eventSink: (Event) -> Unit) : CircuitUiState
+
+ sealed class Event : CircuitUiEvent {
+ data class EmailClicked(val emailId: String) : Event()
+ }
+}
+
+class InboxPresenter(
+ private val navigator: Navigator,
+ private val emailRepository: EmailRepository,
+) : Presenter {
+ @Composable
+ override fun present(): InboxScreen.State {
+ val emails by
+ produceState>(initialValue = emptyList()) { value = emailRepository.getEmails() }
+ return InboxScreen.State(emails) { event ->
+ when (event) {
+ is InboxScreen.Event.EmailClicked -> navigator.goTo(DetailScreen(event.emailId))
+ }
+ }
+ }
+
+ class Factory(private val emailRepository: EmailRepository) : Presenter.Factory {
+ override fun create(
+ screen: Screen,
+ navigator: Navigator,
+ context: CircuitContext,
+ ): Presenter<*>? {
+ return when (screen) {
+ InboxScreen -> return InboxPresenter(navigator, emailRepository)
+ else -> null
+ }
+ }
+ }
+}
+
+@Composable
+fun Inbox(state: InboxScreen.State, modifier: Modifier = Modifier) {
+ Scaffold(modifier = modifier, topBar = { TopAppBar(title = { Text("Inbox") }) }) { innerPadding ->
+ LazyColumn(modifier = Modifier.padding(innerPadding)) {
+ items(state.emails) { email ->
+ EmailItem(
+ email = email,
+ onClick = { state.eventSink(InboxScreen.Event.EmailClicked(email.id)) },
+ )
+ }
+ }
+ }
+}
diff --git a/samples/tutorial/src/jvmMain/kotlin/com/slack/circuit/tutorial/sharedelements/main.kt b/samples/tutorial/src/jvmMain/kotlin/com/slack/circuit/tutorial/sharedelements/main.kt
new file mode 100644
index 000000000..28c5ca067
--- /dev/null
+++ b/samples/tutorial/src/jvmMain/kotlin/com/slack/circuit/tutorial/sharedelements/main.kt
@@ -0,0 +1,40 @@
+// Copyright (C) 2024 Slack Technologies, LLC
+// SPDX-License-Identifier: Apache-2.0
+package com.slack.circuit.tutorial.sharedelements
+
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.ui.window.Window
+import androidx.compose.ui.window.application
+import com.slack.circuit.backstack.rememberSaveableBackStack
+import com.slack.circuit.foundation.Circuit
+import com.slack.circuit.foundation.CircuitCompositionLocals
+import com.slack.circuit.foundation.NavigableCircuitContent
+import com.slack.circuit.foundation.rememberCircuitNavigator
+import com.slack.circuit.sharedelements.SharedElementTransitionLayout
+import com.slack.circuit.tutorial.common.EmailRepository
+
+@OptIn(ExperimentalSharedTransitionApi::class)
+fun main() {
+ val emailRepository = EmailRepository()
+ val circuit: Circuit =
+ Circuit.Builder()
+ .addPresenterFactory(DetailPresenter.Factory(emailRepository))
+ .addPresenterFactory(InboxPresenter.Factory(emailRepository))
+ .addUi { state, modifier -> Inbox(state, modifier) }
+ .addUi { state, modifier -> EmailDetail(state, modifier) }
+ .build()
+ application {
+ Window(title = "Tutorial", onCloseRequest = ::exitApplication) {
+ MaterialTheme {
+ val backStack = rememberSaveableBackStack(InboxScreen)
+ val navigator = rememberCircuitNavigator(backStack) { exitApplication() }
+ CircuitCompositionLocals(circuit) {
+ SharedElementTransitionLayout {
+ NavigableCircuitContent(navigator = navigator, backStack = backStack)
+ }
+ }
+ }
+ }
+ }
+}