Routing functionality for Jetpack Compose with back stack

Overview

compose-router

Build Version License

logo

What's this?

Routing functionality for Jetpack Compose with back stack:

  • Helps to map your whole app structure using Compose — not just the UI parts
  • Supports a single-Activity approach — no Fragments, no Navigation component needed
  • Simply branch on current routing and compose any other @Composable
  • Back stack saves the history of routing
  • Can be integrated with automatic back press handling to go back in screen history
  • Can be integrated with automatic scoped savedInstanceState persistence
  • Supports routing based on deep links (POC impl)

Compatible with Compose version 1.0

Sample apps

  1. Sample module #1 - app-lifelike — Displays a registration flow + logged in content with back stack

  2. Sample module #2 - app-nested-containers — Displays nested screen history on generated levels.

  3. Jetnews - fork — Built with compose-router, adding proper screen history functionality.

  4. Pokedex - compose-pokedex — Using compose-router for app structure.

Download

Available through jitpack.

Add the maven repo to your root build.gradle

allprojects {
    repositories {
        maven { url 'https://jitpack.io' }
    }
}

Add the dependency:

implementation 'com.github.zsoltk:compose-router:{latest-version}'

How to use

On any level where routing functionality is needed, create a sealed class to represent your routing:

sealed class Routing {
    object AlbumList : Routing()
    data class PhotosOfAlbum(val album: Album) : Routing()
    data class FullScreenPhoto(val photo: Photo) : Routing()
}

Use the Router Composable and enjoy back stack functionality:

@Composable
fun GalleryView(defaultRouting: Routing) {
    Router("GalleryView", defaultRouting) { backStack ->
        // compose further based on current routing:
        when (val routing = backStack.last()) {
            is Routing.AlbumList -> AlbumList.Content(
                onAlbumSelected = {
                    // add a new routing to the back stack:
                    backStack.push(Routing.PhotosOfAlbum(it))
                })

            is Routing.PhotosOfAlbum -> PhotosOfAlbum.Content(
                album = routing.album,
                onPhotoSelected = {
                    // add a new routing to the back stack:
                    backStack.push(Routing.FullScreenPhoto(it))
                })

            is Routing.FullScreenPhoto -> FullScreenPhoto.Content(
                photo = routing.photo
            )
        }
    }
}

For more usage examples see the example apps.

To go back in the back stack, you can either call the .pop() method programmatically, or just press the back button on the device (see next section for back press integration).

Back stack operations:

  • push()
  • pushAndDropNested()
  • pop()
  • replace()
  • newRoot()

Connect it to back press event

To ensure that back press automatically pops the back stack and restores history, add this to your Activity:

class MainActivity : AppCompatActivity() {
    private val backPressHandler = BackPressHandler()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Providers(
                LocalBackPressHandler provides backPressHandler
            ) {
                // Your root composable goes here
            }
        }
    }

    override fun onBackPressed() {
        if (!backPressHandler.handle()) {
            super.onBackPressed()
        }
    }
}

Connect it to savedInstanceState

Router can automatically add scoped Bundle support for your client code.

Minimal setup:

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                BundleScope(savedInstanceState) {
                    // Your root composable goes here
                }
            }
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.saveLocal()
    }
}

In client code you can now use:

@Composable
fun Content() {
    var counter by persistentInt("counter", 0)

    Clickable(onClick = { counter++ }) {
        Text("Counter value saved/restored from bundle: $counter")
    }
}

Routing from deep links

Note: this is even more of a proof-of-concept only implementation than the other parts.

Example 1

Build and install app-lifelike on your device.

Open a console and type:

adb shell 'am start -a "android.intent.action.VIEW" -d "app-lifelike://go-to-profile?name=fake&phone=123123"'

This will open app-lifelike with skipped registration flow and go directly to Profile screen with fake user:

Example 2

Build and install app-nested-containers on your device.

Open a console and type:

adb shell 'am start -a "android.intent.action.VIEW" -d "app-nested://default/BGR"'

This will open app-nested-containers with (B)lue / (G)reen / (R)ed subtrees pre-selected as routing:

See MainActivity.kt, AndroidManifest.xml, and DeepLink.kt in both sample apps to see usage example.

Comments
  • Navigation screens seem to be duplicated

    Navigation screens seem to be duplicated

    I'm trying out your library (0.5.0) and it's great. But I ran into an issue. I will post code here to reproduce it. I might be using the library wrong though.

    class MainActivity : AppCompatActivity() {
        private val backPressHandler = BackPressHandler()
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                MaterialTheme {
                    Providers(AmbientBackPressHandler provides backPressHandler) {
                        MyRouter(startRoute = SplashRoute)
                    }
                }
            }
        }
    
        override fun onBackPressed() {
            if (!backPressHandler.handle()) {
                super.onBackPressed()
            }
        }
    
        override fun onSaveInstanceState(outState: Bundle) {
            super.onSaveInstanceState(outState)
            outState.saveAmbient()
        }
    }
    
    sealed class MyRoute {
        object SplashRoute : MyRoute()
        object HomeRoute : MyRoute()
        object DetailsRoute : MyRoute()
    }
    
    @Composable
    fun MyRouter(startRoute: MyRoute) {
        Router(contextId = "app", defaultRouting = startRoute) { backStack ->
            when (backStack.last()) {
                SplashRoute -> SplashRouteScreen(onInitialized = { backStack.newRoot(HomeRoute) })
                HomeRoute -> HomeRouteScreen(onShowDetails = { backStack.push(DetailsRoute) })
                DetailsRoute -> DetailsRouteScreen()
            }
        }
    }
    
    @Composable
    fun SplashRouteScreen(onInitialized: () -> Unit) {
        onActive {
            onInitialized()
        }
    }
    
    @Composable
    fun HomeRouteScreen(onShowDetails: () -> Unit) {
        onActive {
            Log.d("HomeRouteScreen", "onActive ${System.currentTimeMillis()}")
    
            onDispose {
                Log.d("HomeRouteScreen", "onDispose ${System.currentTimeMillis()}")
            }
        }
    
        Center {
            Button(onClick = onShowDetails) {
                Text(text = "Show details")
            }
        }
    }
    
    @Composable
    fun DetailsRouteScreen() {
        Center {
            Text("Details")
        }
    }
    

    So if you open the app and you navigate through screens everything is fine. Logs in HomeRouteScreen work as intended. But if you leave the app with back button (don't kill the app), then:

    1. Open app again
    2. Tap "Show details"
    3. Press back

    Logs will look like this:

    D/HomeRouteScreen: onActive 1582533598703
    D/HomeRouteScreen: onDispose 1582533600692
    D/HomeRouteScreen: onDispose 1582533600697
    D/HomeRouteScreen: onActive 1582533602550
    D/HomeRouteScreen: onActive 1582533602551
    

    If you leave the app again, and repeat those steps:

    HomeRouteScreen: onActive 1582533684493
    HomeRouteScreen: onDispose 1582533699006
    HomeRouteScreen: onDispose 1582533699009
    HomeRouteScreen: onDispose 1582533699011
    HomeRouteScreen: onActive 1582533699988
    HomeRouteScreen: onActive 1582533699989
    HomeRouteScreen: onActive 1582533699989
    

    Seems like every time you close the app with back button then reopen, something gets duplicated.

    bug 
    opened by mvarnagiris 17
  • How do I save the state in a previous view?

    How do I save the state in a previous view?

    Assuming you have two destinations, A and B. A displays a long list. You click on an item in the list to navigate to B. Once in B you click on the back button. The A view is entirely recreated. The list is at the top again. I am unsure if this is by design or not.

    sealed class Routing {
      object A: Routing()
      data class B(val slug: String) : Routing()
    }
    @Composable
    fun AppStore(routing: Routing = Routing.A) {
      Router(routing) { backStack ->
        val onItemClicked: (String) = { slug ->
          backStack.push(Routing.B(slug))
        }
        when (val value = backStack.last()) {
          is Routing.A-> AController(onItemClicked)
          is Routing.B-> BController(value.slug)
        }
      }
    }
    

    I've already added a rememberLazyListState but this seems to be recreated entirely as well

    I am using version 0.24.2

    opened by zunjae 9
  • Support for compose 0.1.0-dev16

    Support for compose 0.1.0-dev16

    Hello, I noticed this library does not work on jetpack-compose dev16 (with errors like Composable functions can only be used from other Composable functions.). This is probably because @Composable annotation had been refactored to another package in dev16 and so the compiler cannot match the two different @Composable annotations.

    In this MR I have updated the dependency version to 0.1.0-dev16 and adjusted all the imports. It compiles on dev16 and works in my application.

    opened by dshatz 5
  • Top of Stack

    Top of Stack

    Hi,

    I'd find it really useful to know what the top item in the backstack is - I'm using bottom navigation and knowing the top item on the backstack would help in deciding which item at the bottom should be highlighted (even when a few destinations deep in that stack).

    Is there a reason this shouldn't be exposed?

    opened by notquiteamonad 5
  • update to dev13

    update to dev13

    Updates to dev13, update compile and target sdk to 30.

    I know you did not want the samples to reference the router module directly. And it should instead reference an existing release. But after being forced to update gradle, the only way I could get the lifelike module to compile was to update it to dev13. Which meant it needed access to router built with dev13.

    Also I couldn't get the nested sample to build using dev09 with the gradle update so I was able to update that one to dev12

    opened by luca992 5
  • Crash with Jetpack Compose Alpha07

    Crash with Jetpack Compose Alpha07

    Version of Jetpack Compose: alpha07 Version of Compose router: 0.21.0

    The following runtime error is thrown

     java.lang.NoSuchFieldError: No field Companion of type Landroidx/compose/runtime/SlotTable$Companion; in class Landroidx/compose/runtime/SlotTable; or its superclasses (declaration of 'androidx.compose.runtime.SlotTable' appears in /data/data/com.metamythlabs.theseries/code_cache/.overlay/base.apk/classes.dex)
            at com.github.zsoltk.compose.router.RouterKt.Router(Router.kt:105)
            at com.metamythlabs.theseries.screens.MainRouter$Companion.Content(MainRouter.kt:28)
    

    Usage of router (excerpt)

     companion object {
            @Composable
            fun Content(defaultRouting: Routing = Routing.Home) {
                Router("Main", defaultRouting) { backStack ->
    
                    Providers(
                        AppNavigatorAmbient provides TheSeenNavigator(backStack)
                    ) {
                        when (val routing = backStack.last()) {
                            is Routing.Home -> HomeRouter.Content()
                            is Routing.MoviesDetail -> MovieRouter.Content(movieId = routing.movieId)
                            is Routing.SeriesDetail -> SeriesRouter.Content(seriesId = routing.seriesId)
                            is Routing.UniverseDetail -> UniverseRouter.Content(universeId = routing.universeId)
                        }
                    }
                }
            }
        }
    }
    

    Sadly I couldn't solve it quickly, hence I wanna report it.

    opened by johnnycube 4
  • Updated for Jetpack Compose dev15 and Android Studio 4.2 Canary 5

    Updated for Jetpack Compose dev15 and Android Studio 4.2 Canary 5

    I got everything working, however I can't figure out the correct way to handle the three unchecked casts properly. I left the @Suppress("UNCHECKED_CAST") commented near each one.

    I guess this is something to do with Covariance / Contravariance etc, but I dont know enough about how to solve it. Seems theres a List and a Map with BackStack<*>. Would one solution be to have some kind of known Routing interface or class which is subclassed or implemented so that the type and its sub/super are known?

    I would be interested in knowing how this should be fixed so that I can understand this whole <*> thing in Kotlin.

    opened by madhavajay 4
  • [Proposal] Expose read access for BackStack#elements

    [Proposal] Expose read access for BackStack#elements

    I wonder if there is any way to retrieve all elements from BackStack? It can be useful in the following cases:

    1. We want to render all screens from BackStack on top of each other, so after navigating back we will not re-render some screens.
    2. Integration with compose-backstack (library for rendering animated transitions between backstacks of screens) would be much simpler, because nowadays it requires list of screens as one of args to its Composable. There are some workarounds, such as using Kotlin Reflection or using state/remember for maintaining backstack yourself, but it would be great to have read access directly to BackStack#elements.

    @zsoltk WDYT?

    opened by friendoye 3
  • Unable to import when built with com.android.tools.build:gradle > 4.1.0-alpha05

    Unable to import when built with com.android.tools.build:gradle > 4.1.0-alpha05

    I mentioned this bug in https://github.com/zsoltk/compose-router/pull/21. Probably needs to be reported to Google. It still does not work with 4.1.0-alpha09.

    For some reason with com.android.tools.build:gradle versions 4.1.0-alpha06 - 4.1.0-alpha08 the META-INF folder isn't added to the classes.jar in the .aar outputs

    Screenshot from 2020-05-11 16-30-42

    Screenshot from 2020-05-11 16-26-20

    My branch built with com.android.tools.build:gradle:4.1.0-alpha05 includes the META-INF folder and I can successfully build my project Screenshot from 2020-05-11 16-27-40

    opened by luca992 3
  • Update for compose release 0.1.0-dev07

    Update for compose release 0.1.0-dev07

    Update gradle to 4.1.0-alpha03 Change left/right to start/end Use new package for Surface component DrawImage -> Image LayoutFlexible -> LayoutWeight

    opened by cgudea 3
  • Implicit id for Router

    Implicit id for Router

    Right now Router needs an explicit unique id for back stack store/retrieval from parent. It would be nice to make this implicitly without having to ask client code to define id.

    enhancement help wanted 
    opened by zsoltk 3
  • ViewModels not cleared when navigation out

    ViewModels not cleared when navigation out

    The ViewModels provided in the composition by hilt, are never destroyed when a new root destination is selected. With compose navigation this is different because models are cleared as soon as the destination gets cleared from the back stack.

    opened by NestorPerez13 0
  • Support for Dynamic Features?

    Support for Dynamic Features?

    With deeplink I think it's support between Dynamice Feature navigation, cmiiw or does it requires any setup/configuration differences than using Deeplink?

    opened by mochadwi 0
  • How to handle TopAppBar navigation back event?

    How to handle TopAppBar navigation back event?

    Initial setup of BackPressHandler in the MainActivity works well with navigation but how to handle Appbar navback button click event? Now I've forked and created RootBackPressHandler Ambient which is set once in root Router and then I'm accessing it from anywhere

    opened by rustamsmax 0
  • Crash with 0.11.1

    Crash with 0.11.1

    Version 0.11.1 crashes withbackPressHandler is not initialized at a certain route depth. Downgrading to 0.9.0 fixes the issue. I'm having trouble reproducing this properly.

    opened by theolampert 0
Owner
Zsolt Kocsi
Zsolt Kocsi
It's a simple app written in Kotlin that shows a simple solution for how to save an image into Firebase Storage, save the URL in Firestore, and read it back using Jetpack Compose.

It's a simple app written in Kotlin that shows a simple solution for how to save an image into Firebase Storage, save the URL in Firestore, and read it back using Jetpack Compose.

Alex 10 Dec 29, 2022
Jetpack Compose based project, used to stress-testing compose features / integrations and explore non-trivial functionality

Project containing Jetpack Compose samples For pagination & network images it uses CATAAS. Known issues Navigation-Compose Issue with fast tapping on

Denis Rudenko 59 Dec 14, 2022
CoinList is a simple app based on Jetpack Compose, modern tech-stack and Clean Architecture.

CoinList is a simple app based on Jetpack Compose, modern tech-stack and Clean Architecture. The project is quite simple which shows the list of crypto currencies and their general info details gets from API.

Yıldırım Tam 18 Aug 28, 2022
Jet-CoinList is a simple app based on Jetpack Compose, modern tech-stack and Clean Architecture.

Jet-CoinList is a simple app based on Jetpack Compose, modern tech-stack and Clean Architecture. The project is quite simple which shows the list of crypto currencies and their general info details gets from API.

Yıldırım Tam 18 Aug 28, 2022
PapriCoin demonstrates Jetpack Compose usage to build modern app based on Clean Architecture and newest Tech-Stack

PapriCoin demonstrates Jetpack Compose usage to build modern app based on Clean Architecture and newest Tech-Stack. Repository also has loca

Malik Mukhametzyanov 15 Nov 9, 2022
Recreated iOS Calculator UI and functionality for android with Jetpack Compose

Compose-iOS-Calculator Recreated iOS Calculator UI and functionality for android with Jetpack Compose Currently using Regex to do the math, but when I

Ikechukwu Eze 6 Oct 11, 2022
Preference functionality for Jetpack Compose

ComposePreferences Compose Preferences is a library which makes it easy to add preference functionality to your Compose app. It provides an easy to us

Niklas Schnettler 59 Jan 4, 2023
Simple-stack FTUE sample using Compose integration (and Rx)

Sample code using simple-stack and the Compose integration to display "First-Time User Experience", based on an older version of "Conditional Navigation" section of Jetpack Navigation documentation.

Gabor Varadi 3 Aug 15, 2021
Zoom Modifiers, zoomable image and layouts with limit pan bounds, fling and moving back to valid bounds and callbacks that return current transformation or visible image section

Zoom Modifiers, zoomable image and layouts with limit pan bounds, fling and moving back to valid bounds and callbacks that return current transformation or visible image section

Smart Tool Factory 20 Dec 13, 2022
ComposeTextBug - Issue with MotionLayout+Compose in Text functionality

ComposeTextBug Issue with MotionLayout+Compose in Text functionality Demo: devic

Yahor 0 Jan 12, 2022
Jetpack Compose Boids | Flocking Insect 🐜. bird or Fish simulation using Jetpack Compose Desktop 🚀, using Canvas API 🎨

?? ?? ?? Compose flocking Ants(boids) ?? ?? ?? Jetpack compose Boids | Flocking Insect. bird or Fish simulation using Jetpack Compose Desktop ?? , usi

Chetan Gupta 38 Sep 25, 2022
A collection of animations, compositions, UIs using Jetpack Compose. You can say Jetpack Compose cookbook or play-ground if you want!

Why Not Compose! A collection of animations, compositions, UIs using Jetpack Compose. You can say Jetpack Compose cookbook or play-ground if you want!

Md. Mahmudul Hasan Shohag 186 Jan 1, 2023
Learn Jetpack Compose for Android by Examples. Learn how to use Jetpack Compose for Android App Development. Android’s modern toolkit for building native UI.

Learn Jetpack Compose for Android by Examples. Learn how to use Jetpack Compose for Android App Development. Android’s modern toolkit for building native UI.

MindOrks 382 Jan 5, 2023
This is a sample app(For beginners - App #2) built using Jetpack Compose. It demonstrates the concept of State Hoisting in Jetpack Compose.

JetBMICalculator This is a sample app(For beginners - App #2) built using Jetpack Compose. It demonstrates the concept of State Hoisting in Jetpack Co

BHAVNA THACKER 3 Dec 31, 2022
Jetpack-Compose-Demo - Instagram Profile UI using Jetpack Compose

Jetpack-Compose-Demo Instagram Profile UI using Jetpack Compose

omar 1 Aug 11, 2022
Jetpack-compose-animations-examples - Cool animations implemented with Jetpack compose

Jetpack-compose-animations-examples This repository consists of 4 animations: St

Canopas Software 180 Jan 2, 2023
Compose-navigation - Set of utils to help with integrating Jetpack Compose and Jetpack's Navigation

Jetpack Compose Navigation Set of utils to help with integrating Jetpack Compose

Adam Kobus 5 Apr 5, 2022
Jetpack-compose-uis - A collection of some UIs using Jetpack Compose. built using Katalog

Jetpack Compose UIs This is a collection of some UIs using Jetpack Compose. It i

Mori Atsushi 3 Dec 15, 2022
A simple authentication application using Jetpack compose to illustrate signin and sign up using Mvvm, Kotlin and jetpack compose

Authentication A simple authentication application using Jetpack compose to illustrate signin and sign up using Mvvm, Kotlin and jetpack compose Scree

Felix Kariuki 5 Dec 29, 2022