diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ad74e41
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,14 @@
+*.iml
+.gradle
+/local.properties
+/.idea/caches
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+/.idea/navEditor.xml
+/.idea/assetWizardSettings.xml
+.DS_Store
+build/
+/captures
+.externalNativeBuild
+.cxx
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..fe1730d
--- /dev/null
+++ b/README.md
@@ -0,0 +1,50 @@
+# compose-backstack
+
+Simple library for [Jetpack Compose](https://developer.android.com/jetpack/compose) for rendering
+backstacks of screens and animated transitions when the stack changes. It is _not_ a navigation
+library, although it is meant to be easy to plug into your navigation library of choice
+(e.g. [compose-router](https://github.com/zsoltk/compose-router)), or even just use on its own.
+
+This library is compatible with Compose dev06.
+
+## Usage
+
+The entry point to the library is the `Backstack` composable.
+
+## Example
+
+```kotlin
+ sealed class Screen {
+ object ContactList: Screen()
+ data class ContactDetails(val id: String): Screen()
+ data class EditContact(val id: String): Screen()
+ }
+
+ data class Navigator(
+ val push: (Screen) -> Unit,
+ val pop: () -> Unit
+ )
+
+ @Composable fun App() {
+ var backstack by state { listOf(Screen.ContactList) }
+ val navigator = remember {
+ Navigator(
+ push = { backstack += it },
+ pop = { backstack = backstack.dropLast(1) }
+ )
+ }
+
+ Backstack(backstack) { screen ->
+ when(screen) {
+ Screen.ContactList -> ShowContactList(navigator)
+ is Screen.ContactDetails -> ShowContact(screen.id, navigator)
+ is Screen.EditContact -> ShowEditContact(screen.id, navigator)
+ }
+ }
+ }
+```
+
+## Samples
+
+There is a sample app in the `sample` module that demonstrates various transition animations and
+the behavior with different backstacks.
diff --git a/backstack/build.gradle b/backstack/build.gradle
new file mode 100644
index 0000000..155d939
--- /dev/null
+++ b/backstack/build.gradle
@@ -0,0 +1,11 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android rootProject.ext.defaultAndroidConfig
+
+dependencies {
+ compileOnly deps.compose.tooling
+
+ implementation deps.kotlin.stdlib
+ implementation deps.compose.foundation
+}
diff --git a/backstack/consumer-rules.pro b/backstack/consumer-rules.pro
new file mode 100644
index 0000000..e69de29
diff --git a/backstack/proguard-rules.pro b/backstack/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/backstack/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/backstack/src/main/AndroidManifest.xml b/backstack/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..4bb8456
--- /dev/null
+++ b/backstack/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+
diff --git a/backstack/src/main/java/com/zachklipp/compose/backstack/Backstack.kt b/backstack/src/main/java/com/zachklipp/compose/backstack/Backstack.kt
new file mode 100644
index 0000000..bcc4f20
--- /dev/null
+++ b/backstack/src/main/java/com/zachklipp/compose/backstack/Backstack.kt
@@ -0,0 +1,243 @@
+@file:Suppress("RemoveEmptyParenthesesFromAnnotationEntry", "FunctionName")
+
+package com.zachklipp.compose.backstack
+
+import androidx.animation.AnimationBuilder
+import androidx.animation.AnimationEndReason
+import androidx.animation.AnimationEndReason.TargetReached
+import androidx.animation.TweenBuilder
+import androidx.compose.Composable
+import androidx.compose.key
+import androidx.compose.remember
+import androidx.compose.state
+import androidx.ui.animation.animatedFloat
+import androidx.ui.core.Clip
+import androidx.ui.core.ContextAmbient
+import androidx.ui.core.Modifier
+import androidx.ui.foundation.Box
+import androidx.ui.foundation.shape.RectangleShape
+import androidx.ui.layout.Stack
+import com.zachklipp.compose.backstack.TransitionDirection.Backward
+import com.zachklipp.compose.backstack.TransitionDirection.Forward
+
+/**
+ * Identifies which direction a transition is being performed in.
+ */
+enum class TransitionDirection {
+ Forward,
+ Backward
+}
+
+/**
+ * Wraps each screen composable with the transition modifier derived from the current animation
+ * progress.
+ */
+private data class ScreenWrapper(
+ val key: T,
+ val transition: @Composable() (progress: Float, @Composable() () -> Unit) -> Unit
+)
+
+@Composable
+private val DefaultBackstackAnimation: AnimationBuilder
+ get() {
+ val context = ContextAmbient.current
+ return TweenBuilder().apply {
+ duration = context.resources.getInteger(android.R.integer.config_shortAnimTime)
+ }
+ }
+
+/**
+ * Renders the top of a stack of screens (as [T]s) and animates between screens when the top
+ * value changes. Any state used by a screen will be preserved as long as it remains in the stack
+ * (i.e. result of [remember] or [state] calls).
+ *
+ * The [backstack] must follow some rules:
+ * - Must always contain at least one item.
+ * - Elements in the stack must implement `equals` and not change over the lifetime of the screen.
+ * If the key changes, it will be considered a new screen and any state held by the screen will
+ * be lost.
+ * - If items in the stack are reordered between compositions, the stack should not contain
+ * duplicates. If it does, due to how `@Pivotal` works, the states of those screens will be
+ * lost if they are moved around. Duplicates should retain state if they are not reordered.
+ *
+ * This composable does not actually provide any navigation functionality – it just renders
+ * transitions between stacks of screens. It can be plugged into your navigation library of choice,
+ * or just used on its own with a simple list of screens, like this:
+ *
+ * ```
+ * sealed class Screen {
+ * object ContactList: Screen()
+ * data class ContactDetails(val id: String): Screen()
+ * data class EditContact(val id: String): Screen()
+ * }
+ *
+ * data class Navigator(
+ * val push: (Screen) -> Unit,
+ * val pop: () -> Unit
+ * )
+ *
+ * @Composable fun App() {
+ * var backstack by state { listOf(Screen.ContactList) }
+ * val navigator = remember {
+ * Navigator(
+ * push = { backstack += it },
+ * pop = { backstack = backstack.dropLast(1) }
+ * )
+ * }
+ *
+ * Backstack(backstack) { screen ->
+ * when(screen) {
+ * Screen.ContactList -> ShowContactList(navigator)
+ * is Screen.ContactDetails -> ShowContact(screen.id, navigator)
+ * is Screen.EditContact -> ShowEditContact(screen.id, navigator)
+ * }
+ * }
+ * }
+ * ```
+ *
+ * @param backstack The stack of screen values.
+ * @param modifier [Modifier] that will be applied to the container of screens. Neither affects nor
+ * is affected by transition animations.
+ * @param transition The [BackstackTransition] that defines how to animate between screens when
+ * [backstack] changes. [BackstackTransition] contains a few simple pre-fab transitions.
+ * @param animationBuilder Defines the curve and speed of transition animations.
+ * @param onTransitionStarting Callback that will be invoked before starting each transition.
+ * @param onTransitionFinished Callback that will be invoked after each transition finishes.
+ * @param drawScreen Called with each element of [backstack] to render it.
+ */
+@Composable
+fun Backstack(
+ backstack: List,
+ modifier: Modifier = Modifier.None,
+ transition: BackstackTransition = BackstackTransition.Slide,
+ animationBuilder: AnimationBuilder? = null,
+ onTransitionStarting: ((from: List, to: List, TransitionDirection) -> Unit)? = null,
+ onTransitionFinished: (() -> Unit)? = null,
+ drawScreen: @Composable() (T) -> Unit
+) {
+ require(backstack.isNotEmpty()) { "Backstack must contain at least 1 screen." }
+
+ // When transitioning, contains a stable cache of the screens actually being displayed. Will not
+ // change even if backstack changes during the transition.
+ var activeKeys by state { backstack }
+ // The "top" screen being transitioned to. Used at the end of the transition to detect if the
+ // backstack changed and needs another transition immediately.
+ var targetTop by state { backstack.last() }
+ // Wrap all items to draw in a list, so that they will all share a constant "compositional
+ // position", which allows us to use @Pivotal machinery to preserve state.
+ var activeStackDrawers by state { emptyList>() }
+ // Defines the progress of the current transition animation in terms of visibility of the top
+ // screen. 1 means top screen is visible, 0 means top screen is entirely hidden. Must be 1 when
+ // no transition in progress.
+ val transitionProgress = animatedFloat(1f)
+ // Null means not transitioning.
+ var direction by state { null }
+ // Callback passed to animations to cleanup after the transition is done.
+ val onTransitionEnd = remember {
+ { reason: AnimationEndReason, _: Float ->
+ if (reason == TargetReached) {
+ direction = null
+ transitionProgress.snapTo(1f)
+ onTransitionFinished?.invoke()
+ }
+ }
+ }
+ val animation = animationBuilder ?: DefaultBackstackAnimation
+
+ if (direction == null && activeKeys != backstack) {
+ // Not in the middle of a transition and we got a new backstack.
+ // This will also run after a transition, to clean up old keys out of the temporary backstack.
+
+ if (backstack.last() == targetTop) {
+ // Don't need to transition, but some hidden keys changed to so we need to update the active
+ // list to ensure hidden screens that no longer exist are torn down.
+ activeKeys = backstack
+ } else {
+ // Remember the top we're transitioning to so we don't re-transition afterwards if we're
+ // showing the same top.
+ targetTop = backstack.last()
+
+ // If the new top is in the old backstack, then it has probably already been seen, so the
+ // navigation is logically backwards, even if the new backstack actually contains more
+ // screens.
+ direction = if (targetTop in activeKeys) Backward else Forward
+
+ // Mutate the stack for the transition so the keys that need to be temporarily shown are in
+ // the right place.
+ val oldTop = activeKeys.last()
+ val newKeys = backstack.toMutableList()
+ if (direction == Backward) {
+ // We need to put the current screen on the top of the new active stack so it will animate
+ // out.
+ newKeys += oldTop
+
+ // When going back the top screen needs to start off as visible.
+ transitionProgress.snapTo(1f)
+ transitionProgress.animateTo(0f, anim = animation, onEnd = onTransitionEnd)
+ } else {
+ // If the current screen is not the new second-last screen, we need to move it to that
+ // position so it animates out when going forward. This is true whether or not the current
+ // screen is actually in the new backstack at all.
+ newKeys -= targetTop
+ newKeys -= oldTop
+ newKeys += oldTop
+ newKeys += targetTop
+
+ // When going forward, the top screen needs to start off as invisible.
+ transitionProgress.snapTo(0f)
+ transitionProgress.animateTo(1f, anim = animation, onEnd = onTransitionEnd)
+ }
+ onTransitionStarting?.invoke(activeKeys, backstack, direction!!)
+ activeKeys = newKeys
+ }
+ }
+
+ // Only refresh the wrappers when the keys or opacity actually change.
+ // We need to regenerate these if the keys in the backstack change even if the top doesn't change
+ // because we need to dispose of old screens that are no longer rendered.
+ //
+ // Note: This block must not contain any control flow logic that causes the screen composables
+ // to be invoked from different source locations. If it does, those screens will lose all their
+ // state as soon as a different branch is taken. See @Pivotal for more information.
+ activeStackDrawers = remember(activeKeys, transition) {
+ activeKeys.mapIndexed { index, key ->
+ val isTop = index == activeKeys.size - 1
+ ScreenWrapper(key) { progress, children ->
+ val visibility = when {
+ // transitionProgress always corresponds directly to visibility of the top screen.
+ isTop -> progress
+ // The second-to-top screen has the inverse visibility of the top screen.
+ index == activeKeys.size - 2 -> 1f - progress
+ // All other screens should not be drawn at all. They're only kept around to maintain
+ // their composable state.
+ else -> 0f
+ }
+ val transitionModifier = transition.modifierForScreen(visibility, isTop)
+ Box(transitionModifier, children = children)
+ }
+ }
+ }
+
+ // Actually draw the screens.
+ Stack(modifier = modifier) {
+ // Note: in dev07, this Clip should be replaceable with DrawClipToBounds.
+ Clip(RectangleShape) {
+ activeStackDrawers.forEach { (item, transition) ->
+ // Key is a convenience helper that treats its arguments as @Pivotal. This is how state
+ // preservation is implemented. Even if screens are moved around within the list, as long
+ // as they're invoked through the exact same sequence of source locations from within this
+ // key lambda, they will keep their state.
+ key(item) {
+ // Cache the composable that actually draws this item so it's not recomposed if the
+ // backstack doesn't change. This helps performance with long backstacks.
+ // We don't need to pass item to remember because key guarantees that it won't change
+ // within this part of the composition.
+ val drawItem: @Composable() () -> Unit = remember {
+ @Composable { drawScreen(item) }
+ }
+ transition(transitionProgress.value, drawItem)
+ }
+ }
+ }
+ }
+}
diff --git a/backstack/src/main/java/com/zachklipp/compose/backstack/BackstackTransition.kt b/backstack/src/main/java/com/zachklipp/compose/backstack/BackstackTransition.kt
new file mode 100644
index 0000000..825af21
--- /dev/null
+++ b/backstack/src/main/java/com/zachklipp/compose/backstack/BackstackTransition.kt
@@ -0,0 +1,93 @@
+@file:Suppress("RemoveEmptyParenthesesFromAnnotationEntry")
+
+package com.zachklipp.compose.backstack
+
+import androidx.compose.Composable
+import androidx.ui.core.DrawModifier
+import androidx.ui.core.LayoutModifier
+import androidx.ui.core.Modifier
+import androidx.ui.core.ModifierScope
+import androidx.ui.graphics.Canvas
+import androidx.ui.graphics.Paint
+import androidx.ui.graphics.withSaveLayer
+import androidx.ui.unit.*
+import com.zachklipp.compose.backstack.BackstackTransition.Crossfade
+import com.zachklipp.compose.backstack.BackstackTransition.Slide
+
+/**
+ * Defines transitions for a [Backstack]. Transitions control how screens are rendered by returning
+ * [Modifier]s that will be used to wrap screen composables.
+ *
+ * @see Slide
+ * @see Crossfade
+ */
+interface BackstackTransition {
+
+ /**
+ * Returns a [Modifier] to use to draw screen in a [Backstack].
+ *
+ * @param visibility A float in the range `[0, 1]` that indicates at what visibility this screen
+ * should be drawn. For example, this value will increase when [isTop] is true and the transition
+ * is in the forward direction.
+ * @param isTop True only when being called for the top screen. E.g. if the screen is partially
+ * visible, then the top screen is always transitioning _out_, and non-top screens are either
+ * transitioning out or invisible.
+ */
+ @Composable
+ fun modifierForScreen(
+ visibility: Float,
+ isTop: Boolean
+ ): Modifier
+
+ /**
+ * A simple transition that slides screens horizontally.
+ */
+ object Slide : BackstackTransition {
+ @Composable
+ override fun modifierForScreen(
+ visibility: Float,
+ isTop: Boolean
+ ): Modifier = PercentageLayoutOffset(
+ offset = if (isTop) 1f - visibility else -1 + visibility
+ )
+
+ // Note: In dev07, use LayoutOffset modifier instead.
+ private class PercentageLayoutOffset(private val offset: Float) : LayoutModifier {
+ override fun ModifierScope.modifyPosition(
+ childSize: IntPxSize,
+ containerSize: IntPxSize
+ ): IntPxPosition {
+ val realOffset = offset.coerceIn(-1f..1f)
+ return IntPxPosition(
+ x = containerSize.width * realOffset,
+ y = 0.ipx
+ )
+ }
+ }
+ }
+
+ /**
+ * A simple transition that crossfades between screens.
+ */
+ object Crossfade : BackstackTransition {
+ @Composable
+ override fun modifierForScreen(
+ visibility: Float,
+ isTop: Boolean
+ ): Modifier = OpacityModifier(visibility)
+
+ // Note: In dev07 this modifier is built-in as drawOpacity.
+ private class OpacityModifier(opacity: Float) : DrawModifier {
+ private val paint = Paint().also { it.alpha = opacity }
+
+ override fun draw(
+ density: Density,
+ drawContent: () -> Unit,
+ canvas: Canvas,
+ size: PxSize
+ ) {
+ canvas.withSaveLayer(size.toRect(), paint, drawContent)
+ }
+ }
+ }
+}
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..0673c10
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,102 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+buildscript {
+ ext.versions = [
+ targetSdk: 29,
+ compose: '0.1.0-dev06',
+ kotlin: '1.3.70',
+ ]
+
+ rootProject.ext.defaultAndroidConfig = {
+ compileSdkVersion versions.targetSdk
+ buildToolsVersion '29.0.2'
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ defaultConfig {
+ minSdkVersion 21
+ targetSdkVersion versions.targetSdk
+ versionCode 1
+ versionName "1.0"
+ }
+
+ buildFeatures {
+ compose = true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion versions.compose
+ }
+ }
+
+ ext.deps = [
+ android_gradle_plugin: "com.android.tools.build:gradle:4.1.0-alpha02",
+
+ androidx: [
+ activity: "androidx.activity:activity:1.1.0",
+ annotations: "androidx.annotation:annotation:1.1.0",
+ appcompat: "androidx.appcompat:appcompat:1.1.0",
+ constraint_layout: "androidx.constraintlayout:constraintlayout:1.1.3",
+ fragment: "androidx.fragment:fragment:1.2.2",
+ // Note that we're not using the actual androidx material dep yet, it's still alpha.
+ material: "com.google.android.material:material:1.1.0",
+ recyclerview: "androidx.recyclerview:recyclerview:1.1.0",
+ // Note that we are *not* using lifecycle-viewmodel-savedstate, which at this
+ // writing is still in beta and still fixing bad bugs. Probably we'll never bother to,
+ // it doesn't really add value for us.
+ savedstate: "androidx.savedstate:savedstate:1.0.0",
+ transition: "androidx.transition:transition:1.3.1",
+ viewbinding: "androidx.databinding:viewbinding:3.6.1",
+ ],
+ compose: [
+ foundation: "androidx.ui:ui-foundation:${versions.compose}",
+ icons: "androidx.ui:ui-material-icons-extended:${versions.compose}",
+ layout: "androidx.ui:ui-layout:${versions.compose}",
+ material: "androidx.ui:ui-material:${versions.compose}",
+ test: "androidx.ui:ui-test:${versions.compose}",
+ tooling: "androidx.ui:ui-tooling:${versions.compose}",
+ ],
+ kotlin: [
+ binaryCompatibilityValidatorPlugin: "org.jetbrains.kotlinx:binary-compatibility-validator:0.2.1",
+ gradlePlugin: "org.jetbrains.kotlin:kotlin-gradle-plugin:${versions.kotlin}",
+ stdlib: "org.jetbrains.kotlin:kotlin-stdlib-jdk8",
+ reflect: "org.jetbrains.kotlin:kotlin-reflect:${versions.kotlin}",
+ test: [
+ common: "org.jetbrains.kotlin:kotlin-test-common",
+ annotations: "org.jetbrains.kotlin:kotlin-test-annotations-common",
+ jdk: "org.jetbrains.kotlin:kotlin-test-junit",
+ mockito: "com.nhaarman:mockito-kotlin-kt1.1:1.6.0"
+ ]
+ ],
+ mavenPublish: "com.vanniktech:gradle-maven-publish-plugin:0.9.0",
+ ktlint: "org.jlleitschuh.gradle:ktlint-gradle:9.2.0",
+ ]
+
+ repositories {
+ mavenCentral()
+ gradlePluginPortal()
+ google()
+ }
+
+ dependencies {
+ classpath deps.android_gradle_plugin
+ classpath deps.kotlin.gradlePlugin
+ classpath deps.ktlint
+ }
+}
+
+subprojects {
+ repositories {
+ google()
+ mavenCentral()
+ jcenter()
+ }
+
+ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+ }
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..4d15d01
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,21 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app"s APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
+# Kotlin code style for this project: "official" or "obsolete":
+kotlin.code.style=official
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..f3d88b1
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..84a9066
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.2-all.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..2fe81a7
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,183 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..24467a1
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,100 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/sample/build.gradle b/sample/build.gradle
new file mode 100644
index 0000000..891ba24
--- /dev/null
+++ b/sample/build.gradle
@@ -0,0 +1,20 @@
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+android rootProject.ext.defaultAndroidConfig
+
+android {
+ defaultConfig {
+ applicationId "com.zachklipp.compose.backstack"
+ }
+}
+
+dependencies {
+ implementation project(':backstack')
+ implementation deps.androidx.appcompat
+ implementation deps.compose.icons
+ implementation deps.compose.foundation
+ implementation deps.compose.material
+ implementation deps.compose.tooling
+ implementation deps.kotlin.stdlib
+}
diff --git a/sample/proguard-rules.pro b/sample/proguard-rules.pro
new file mode 100644
index 0000000..481bb43
--- /dev/null
+++ b/sample/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a89f077
--- /dev/null
+++ b/sample/src/main/AndroidManifest.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sample/src/main/java/com/zachklipp/compose/backstack/sample/App.kt b/sample/src/main/java/com/zachklipp/compose/backstack/sample/App.kt
new file mode 100644
index 0000000..85a697b
--- /dev/null
+++ b/sample/src/main/java/com/zachklipp/compose/backstack/sample/App.kt
@@ -0,0 +1,166 @@
+@file:Suppress("RemoveEmptyParenthesesFromAnnotationEntry", "FunctionName")
+
+package com.zachklipp.compose.backstack.sample
+
+import androidx.animation.TweenBuilder
+import androidx.compose.Composable
+import androidx.compose.remember
+import androidx.compose.state
+import androidx.ui.core.DrawModifier
+import androidx.ui.core.Modifier
+import androidx.ui.core.Text
+import androidx.ui.foundation.DrawBorder
+import androidx.ui.graphics.Canvas
+import androidx.ui.graphics.Color
+import androidx.ui.graphics.withSave
+import androidx.ui.layout.*
+import androidx.ui.material.*
+import androidx.ui.material.surface.Surface
+import androidx.ui.tooling.preview.Preview
+import androidx.ui.unit.Density
+import androidx.ui.unit.PxSize
+import androidx.ui.unit.dp
+import com.zachklipp.compose.backstack.Backstack
+import com.zachklipp.compose.backstack.BackstackTransition
+import com.zachklipp.compose.backstack.BackstackTransition.Crossfade
+import com.zachklipp.compose.backstack.BackstackTransition.Slide
+import kotlin.math.pow
+
+private val backstacks = listOf(
+ listOf("one"),
+ listOf("one", "two"),
+ listOf("one", "two", "three"),
+ listOf("two", "one")
+).map { it.joinToString() to it }
+
+private val backstackTransitions = listOf(
+ "Slide" to Slide,
+ "Crossfade" to Crossfade,
+ "Fancy" to FancyTransition
+)
+
+//@Composable
+//fun App() {
+// Center {
+// Text("Hi")
+// }
+//}
+
+@Composable
+fun App() {
+ MaterialTheme(colors = darkColorPalette()) {
+ Surface {
+ var selectedTransition by state { backstackTransitions.first() }
+ var selectedBackstack by state { backstacks.first() }
+ var slowAnimations by state { false }
+ val animation = remember(slowAnimations) {
+ if (slowAnimations) TweenBuilder().apply { duration = 2000 } else null
+ }
+
+ Column(modifier = LayoutSize.Fill) {
+ Text("Backstack transition:")
+ Spinner(
+ items = backstackTransitions,
+ selectedItem = selectedTransition,
+ onSelected = { selectedTransition = it }
+ ) {
+ ListItem(text = it.first)
+ }
+
+ Row {
+ Text("Slow animations:", modifier = LayoutGravity.Center)
+ Switch(slowAnimations, onCheckedChange = { slowAnimations = it })
+ }
+
+ Text("Backstack:")
+ RadioGroup() {
+ backstacks.forEach { backstack ->
+ RadioGroupTextItem(
+ text = backstack.first,
+ textStyle = MaterialTheme.typography().body1,
+ selected = backstack == selectedBackstack,
+ onSelect = { selectedBackstack = backstack }
+ )
+ }
+ }
+
+ MaterialTheme(colors = lightColorPalette()) {
+ Backstack(
+ backstack = selectedBackstack.second,
+ transition = selectedTransition.second,
+ animationBuilder = animation,
+ modifier = LayoutSize.Fill +
+ LayoutPadding(24.dp) +
+ DrawBorder(size = 3.dp, color = Color.Red),
+ onTransitionStarting = { from, to, direction ->
+ println(
+ """
+ Transitioning $direction:
+ from: $from
+ to: $to
+ """.trimIndent()
+ )
+ },
+ onTransitionFinished = { println("Transition finished.") }
+ ) { screen ->
+ AppScreen(
+ name = screen,
+ isLastScreen = screen == selectedBackstack.second.first(),
+ onAdd = {
+ val newBackstack = selectedBackstack.second + "$screen+"
+ selectedBackstack = newBackstack.joinToString() to newBackstack
+ },
+ onBack = {
+ if (selectedBackstack.second.size > 1) {
+ val newBackstack = selectedBackstack.second.dropLast(1)
+ selectedBackstack = newBackstack.joinToString() to newBackstack
+ }
+ }
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+fun AppPreview() {
+ App()
+}
+
+private object FancyTransition : BackstackTransition {
+ @Composable
+ override fun modifierForScreen(
+ visibility: Float,
+ isTop: Boolean
+ ): Modifier {
+ return if (isTop) {
+ Slide.modifierForScreen(visibility.pow(1.1f), isTop) +
+ Crossfade.modifierForScreen(visibility.pow(.1f), isTop)
+ } else {
+ ScaleModifier(visibility.pow(.1f)) +
+ Crossfade.modifierForScreen(visibility.pow(.5f), isTop)
+ }
+ }
+
+ private class ScaleModifier(private val factor: Float) : DrawModifier {
+ override fun draw(
+ density: Density,
+ drawContent: () -> Unit,
+ canvas: Canvas,
+ size: PxSize
+ ) {
+ val halfWidth = size.width.value / 2
+ val halfHeight = size.height.value / 2
+
+ canvas.withSave {
+ canvas.translate(halfWidth, halfHeight)
+ canvas.scale(factor)
+ canvas.translate(-halfWidth, -halfHeight)
+ drawContent()
+ }
+ }
+ }
+}
diff --git a/sample/src/main/java/com/zachklipp/compose/backstack/sample/AppScreen.kt b/sample/src/main/java/com/zachklipp/compose/backstack/sample/AppScreen.kt
new file mode 100644
index 0000000..7965a31
--- /dev/null
+++ b/sample/src/main/java/com/zachklipp/compose/backstack/sample/AppScreen.kt
@@ -0,0 +1,67 @@
+@file:Suppress("FunctionName")
+
+package com.zachklipp.compose.backstack.sample
+
+import android.os.Handler
+import androidx.compose.Composable
+import androidx.compose.Pivotal
+import androidx.compose.onActive
+import androidx.compose.state
+import androidx.ui.core.Text
+import androidx.ui.foundation.Icon
+import androidx.ui.layout.Center
+import androidx.ui.material.FloatingActionButton
+import androidx.ui.material.IconButton
+import androidx.ui.material.Scaffold
+import androidx.ui.material.TopAppBar
+import androidx.ui.material.icons.Icons
+import androidx.ui.material.icons.filled.Add
+import androidx.ui.material.icons.filled.ArrowBack
+import androidx.ui.material.icons.filled.Backup
+import androidx.ui.material.icons.filled.Menu
+import androidx.ui.tooling.preview.Preview
+
+@Preview
+@Composable fun AppScreenPreview() {
+ AppScreen(name = "preview", isLastScreen = false, onBack = {}, onAdd = {})
+}
+
+@Composable fun AppScreen(
+ name: String,
+ isLastScreen: Boolean,
+ onBack: () -> Unit,
+ onAdd: () -> Unit
+) {
+ Scaffold(
+ topAppBar = {
+ val navigationIcon = if (isLastScreen) Icons.Default.Menu else Icons.Default.ArrowBack
+ TopAppBar(
+ navigationIcon = { IconButton(onBack) { Icon(navigationIcon) } },
+ title = { Text(name) })
+ },
+ floatingActionButton = { FloatingActionButton(onClick = onAdd) { Icon(Icons.Default.Add) } }
+ ) {
+ Center {
+ Text(text = "Counter: ${Counter(200)}")
+ }
+ }
+}
+
+@Suppress("SameParameterValue")
+@Composable
+private fun Counter(@Pivotal periodMs: Long): Int {
+ var value by state { 0 }
+ onActive {
+ val mainHandler = Handler()
+ var disposed = false
+ onDispose { disposed = true }
+ fun schedule() {
+ mainHandler.postDelayed({
+ value++
+ if (!disposed) schedule()
+ }, periodMs)
+ }
+ schedule()
+ }
+ return value
+}
diff --git a/sample/src/main/java/com/zachklipp/compose/backstack/sample/ComposeBackstackActivity.kt b/sample/src/main/java/com/zachklipp/compose/backstack/sample/ComposeBackstackActivity.kt
new file mode 100644
index 0000000..90b4abb
--- /dev/null
+++ b/sample/src/main/java/com/zachklipp/compose/backstack/sample/ComposeBackstackActivity.kt
@@ -0,0 +1,14 @@
+package com.zachklipp.compose.backstack.sample
+
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.setViewContent
+import androidx.ui.core.setContent
+import com.zachklipp.compose.backstack.sample.App
+
+class ComposeBackstackActivity : AppCompatActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent { App() }
+ }
+}
diff --git a/sample/src/main/java/com/zachklipp/compose/backstack/sample/Spinner.kt b/sample/src/main/java/com/zachklipp/compose/backstack/sample/Spinner.kt
new file mode 100644
index 0000000..85a5b55
--- /dev/null
+++ b/sample/src/main/java/com/zachklipp/compose/backstack/sample/Spinner.kt
@@ -0,0 +1,72 @@
+@file:Suppress("RemoveEmptyParenthesesFromAnnotationEntry")
+
+package com.zachklipp.compose.backstack.sample
+
+import androidx.compose.Composable
+import androidx.compose.state
+import androidx.ui.foundation.Box
+import androidx.ui.foundation.Clickable
+import androidx.ui.foundation.Dialog
+import androidx.ui.graphics.vector.DrawVector
+import androidx.ui.layout.Column
+import androidx.ui.layout.Container
+import androidx.ui.layout.LayoutAspectRatio
+import androidx.ui.layout.LayoutGravity
+import androidx.ui.layout.LayoutWidth
+import androidx.ui.layout.Row
+import androidx.ui.material.icons.Icons
+import androidx.ui.material.icons.filled.ArrowDropDown
+import androidx.ui.material.ripple.Ripple
+import androidx.ui.material.surface.Surface
+import androidx.ui.unit.dp
+
+/**
+ * Rough implementation of the Android Spinner widget.
+ */
+@Composable fun Spinner(
+ items: List,
+ selectedItem: T,
+ onSelected: (item: T) -> Unit,
+ drawItem: @Composable() (T) -> Unit
+) {
+ if (items.isEmpty()) return
+
+ var isOpen by state { false }
+
+ // Always draw the selected item.
+ Container {
+ Ripple(bounded = true) {
+ Clickable(onClick = { isOpen = !isOpen }) {
+ Row {
+ Box(modifier = LayoutFlexible(1f)) {
+ drawItem(selectedItem)
+ }
+ Box(modifier = LayoutWidth(48.dp) + LayoutAspectRatio(1f) + LayoutGravity.Center) {
+ DrawVector(vectorImage = Icons.Default.ArrowDropDown)
+ }
+ }
+ }
+ }
+
+ if (isOpen) {
+ // TODO use DropdownPopup.
+ Dialog(onCloseRequest = { isOpen = false }) {
+ Surface(elevation = 1.dp) {
+ Column {
+ for (item in items) {
+ Ripple(bounded = true) {
+ Clickable(
+ onClick = {
+ isOpen = false
+ if (item != selectedItem) onSelected(item)
+ }) {
+ drawItem(item)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml b/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..5968f74
--- /dev/null
+++ b/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/sample/src/main/res/drawable/ic_launcher_background.xml b/sample/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..bf1c8bc
--- /dev/null
+++ b/sample/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..03eed25
--- /dev/null
+++ b/sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..03eed25
--- /dev/null
+++ b/sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/sample/src/main/res/mipmap-hdpi/ic_launcher.png b/sample/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a571e60
Binary files /dev/null and b/sample/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..61da551
Binary files /dev/null and b/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/sample/src/main/res/mipmap-mdpi/ic_launcher.png b/sample/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..c41dd28
Binary files /dev/null and b/sample/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..db5080a
Binary files /dev/null and b/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/sample/src/main/res/mipmap-xhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..6dba46d
Binary files /dev/null and b/sample/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..da31a87
Binary files /dev/null and b/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..15ac681
Binary files /dev/null and b/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..b216f2d
Binary files /dev/null and b/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..f25a419
Binary files /dev/null and b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..e96783c
Binary files /dev/null and b/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/sample/src/main/res/values/colors.xml b/sample/src/main/res/values/colors.xml
new file mode 100644
index 0000000..9b4f6b5
--- /dev/null
+++ b/sample/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #6200EE
+ #3700B3
+ #03DAC5
+
\ No newline at end of file
diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml
new file mode 100644
index 0000000..bf7f69a
--- /dev/null
+++ b/sample/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Compose Backstack Sample
+
diff --git a/sample/src/main/res/values/styles.xml b/sample/src/main/res/values/styles.xml
new file mode 100644
index 0000000..ea4f08f
--- /dev/null
+++ b/sample/src/main/res/values/styles.xml
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..43b827f
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,3 @@
+include ':backstack'
+include ':sample'
+rootProject.name = "compose-backstack"
\ No newline at end of file