Code Monkey home page Code Monkey logo

compose-router's Introduction

DEPRECATED

This project is now deprecated. No more development will be taking place here.

The successor of this project is Appyx. Check it out!

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.

compose-router's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

compose-router's Issues

Make example apps independent of library version

Goal
To not block updating the library itself by breaking example apps UI whenever a new compose version is available.

How
Make the example apps grab the built artifact from jitpack instead of compiling from inside the project.

It's understood that this implies that example apps can and will become outdated UI-wise. Their point is to demonstrate Router usage though, and that remains very much the same across Compose versions.

The benefit of being able to update library itself very quickly is worth this trade-off.

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.

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

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.

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

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.

[Question] Integration with ViewModelStore?

I'm wondering if it's possible to adapt compose-router to be able to scope AndroidX ViewModel to a Backstack element?

For example, as long as the route is still in the backstack then the ViewModel is kept alive, and once the route is popped ViewModel's onCleared() gets called.

[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?

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.

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?

Multiplatform support

Would be nice to also support kotlin multiplatform - i.e. kotlin native targets, js and non-android jvm.

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?

Off by one error in replace

My understanding of the replace function on BackStack is that it should swap the item at the top of the stack, so if the routing is a,b,c and replace(d) is called it should become a,b,d.

However, the replace function uses .subList(0, elements.lastIndex - 1) which removes 2 elements (the second argument being exclusive).

This should probably either change to use size or remove the -1 (the latter makes more sense to me).

This is, of course, all invalid if I've misunderstood what replace is supposed to do.

Back stack is not in order

  1. Open nested sample app
  2. Tap L4.R1.NEXT and L4.R2.NEXT buttons alternatively a few times
  3. Start pressing back

What should happen: Back stack should be popped alternatively between L4.R1 and L4.R2
What happens: First L4.R1 gets fully popped, then L4.R2

I created another repository with navigation library where I solved this by having a "Global stack" that handles back stacks from each Router.

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

I mentioned this bug in #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

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.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.