{"id":13604181,"url":"https://github.com/Zhuinden/simple-stack","last_synced_at":"2025-04-11T23:31:59.453Z","repository":{"id":39917167,"uuid":"78756993","full_name":"Zhuinden/simple-stack","owner":"Zhuinden","description":"[ACTIVE] Simple Stack, a backstack library / navigation framework for simpler navigation and state management (for fragments, views, or whatevers).","archived":false,"fork":false,"pushed_at":"2024-05-06T01:11:37.000Z","size":4482,"stargazers_count":1380,"open_issues_count":10,"forks_count":77,"subscribers_count":30,"default_branch":"master","last_synced_at":"2025-04-08T10:18:36.386Z","etag":null,"topics":["android","backstack","fragments","navigation","simple-stack","single-activity-pattern","state-persistence"],"latest_commit_sha":null,"homepage":"","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Zhuinden.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-01-12T15:05:06.000Z","updated_at":"2025-04-06T05:13:21.000Z","dependencies_parsed_at":"2024-10-26T21:16:19.037Z","dependency_job_id":"1b0ef194-0ac1-4afa-8aef-a9752da41e12","html_url":"https://github.com/Zhuinden/simple-stack","commit_stats":null,"previous_names":[],"tags_count":97,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Zhuinden%2Fsimple-stack","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Zhuinden%2Fsimple-stack/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Zhuinden%2Fsimple-stack/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Zhuinden%2Fsimple-stack/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Zhuinden","download_url":"https://codeload.github.com/Zhuinden/simple-stack/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248495056,"owners_count":21113558,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","backstack","fragments","navigation","simple-stack","single-activity-pattern","state-persistence"],"created_at":"2024-08-01T19:00:41.159Z","updated_at":"2025-04-11T23:31:59.431Z","avatar_url":"https://github.com/Zhuinden.png","language":"Java","readme":"![featured](https://androidweekly.net/issues/issue-489/badge)\n[![License](https://img.shields.io/github/license/Zhuinden/simple-stack.svg?style=flat)](https://www.apache.org/licenses/LICENSE-2.0)\n[![](https://jitpack.io/v/Zhuinden/simple-stack.svg)](https://jitpack.io/#Zhuinden/simple-stack)\n\n![simple-stack](simple-stack-logo.png)\n\n# Simple Stack\n\n## Why do I want this?\n\nTo make navigation to another screen as simple as `backstack.goTo(SomeScreen())`, and going back as simple as `backstack.goBack()`.\n\nNo more `FragmentTransaction`s in random places. Predictable and customizable navigation in a single location.\n\n## What is Simple Stack?\n\nSimple Stack is a backstack library (or technically, a navigation framework) that allows you to represent your navigation state in a list of immutable, parcelable data classes (\"keys\").\n\nThis allows preserving your navigation history across configuration changes and process death - this is handled automatically.\n\nEach screen can be associated with a scope, or a shared scope - to easily share data between screens.\n\nThis simplifies navigation and state management within an Activity using either fragments, views, or whatever else.\n\n## Using Simple Stack\n\nIn order to use Simple Stack, you need to add `jitpack` to your project root `build.gradle.kts`\n(or `build.gradle`):\n\n``` kotlin\n// build.gradle.kts\nallprojects {\n    repositories {\n        // ...\n        maven { setUrl(\"https://jitpack.io\") }\n    }\n    // ...\n}\n```\n\nor\n\n``` groovy\n// build.gradle\nallprojects {\n    repositories {\n        // ...\n        maven { url \"https://jitpack.io\" }\n    }\n    // ...\n}\n```\n\nIn newer projects, you need to also update the `settings.gradle` file's `dependencyResolutionManagement` block:\n\n```\ndependencyResolutionManagement {\n    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)\n    repositories {\n        google()\n        mavenCentral()\n        maven { url 'https://jitpack.io' }  // \u003c--\n        jcenter() // Warning: this repository is going to shut down soon\n    }\n}\n```\n\nand then, add the dependency to your module's `build.gradle.kts` (or `build.gradle`):\n\n``` kotlin\n// build.gradle.kts\nimplementation(\"com.github.Zhuinden:simple-stack:2.9.0\")\nimplementation(\"com.github.Zhuinden:simple-stack-extensions:2.3.4\")\n```\n\nor\n\n``` groovy\n// build.gradle\nimplementation 'com.github.Zhuinden:simple-stack:2.9.0'\nimplementation 'com.github.Zhuinden:simple-stack-extensions:2.3.4'\n```\n\n## How do I use it?\n\nYou can check out [**the\ntutorials**](https://github.com/Zhuinden/simple-stack/tree/611e8c7db738a776156b8f709db22b8e37413221/tutorials) for\nsimple examples.\n\n## Fragments\n\nWith Fragments, in `AHEAD_OF_TIME` back handling mode to support predictive back gesture (along\nwith `android:enableBackInvokedCallback`), the Activity code looks like this:\n\n- **With** simple-stack-extensions:lifecycle-ktx\n\n```kotlin\nclass MainActivity : AppCompatActivity(), SimpleStateChanger.NavigationHandler {\n    private lateinit var fragmentStateChanger: FragmentStateChanger\n    private lateinit var backstack: Backstack\n\n    private val backPressedCallback = object : OnBackPressedCallback(false) { // \u003c-- !\n        override fun handleOnBackPressed() {\n            backstack.goBack()\n        }\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        setContentView(R.layout.main_activity)\n\n        onBackPressedDispatcher.addCallback(backPressedCallback) // \u003c-- !\n\n        fragmentStateChanger = FragmentStateChanger(supportFragmentManager, R.id.container)\n\n        backstack = Navigator.configure()\n            .setBackHandlingModel(BackHandlingModel.AHEAD_OF_TIME) // \u003c-- !\n            .setStateChanger(SimpleStateChanger(this))\n            .install(this, binding.container, History.single(HomeKey))\n\n        backPressedCallback.isEnabled = backstack.willHandleAheadOfTimeBack() // \u003c-- !\n        backstack.observeAheadOfTimeWillHandleBackChanged(this, backPressedCallback::isEnabled::set) // \u003c-- ! from lifecycle-ktx\n    }\n    \n    override fun onNavigationEvent(stateChange: StateChange) {\n        fragmentStateChanger.handleStateChange(stateChange)\n    }\n}\n```\n\n- **Without** simple-stack-extensions:lifecycle-ktx\n\n```kotlin\nclass MainActivity : AppCompatActivity(), SimpleStateChanger.NavigationHandler {\n    private lateinit var fragmentStateChanger: FragmentStateChanger\n    private lateinit var backstack: Backstack\n\n    private val backPressedCallback = object : OnBackPressedCallback(false) { // \u003c-- !\n        override fun handleOnBackPressed() {\n            backstack.goBack()\n        }\n    }\n\n    private val updateBackPressedCallback = AheadOfTimeWillHandleBackChangedListener { // \u003c-- !\n        backPressedCallback.isEnabled = it // \u003c-- !\n    }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        setContentView(R.layout.main_activity)\n\n        onBackPressedDispatcher.addCallback(backPressedCallback) // \u003c-- !\n\n        fragmentStateChanger = FragmentStateChanger(supportFragmentManager, R.id.container)\n\n        backstack = Navigator.configure()\n            .setBackHandlingModel(BackHandlingModel.AHEAD_OF_TIME) // \u003c-- !\n            .setStateChanger(SimpleStateChanger(this))\n            .install(this, binding.container, History.single(HomeKey))\n\n        backPressedCallback.isEnabled = backstack.willHandleAheadOfTimeBack() // \u003c-- !\n        backstack.addAheadOfTimeWillHandleBackChangedListener(updateBackPressedCallback) // \u003c-- !\n    }\n\n    override fun onDestroy() {\n        backstack.removeAheadOfTimeWillHandleBackChangedListener(updateBackPressedCallback); // \u003c-- !\n        super.onDestroy()\n    }\n\n    override fun onNavigationEvent(stateChange: StateChange) {\n        fragmentStateChanger.handleStateChange(stateChange)\n    }\n}\n```\n\nWith targetSdkVersion 34 and with `android:enableOnBackInvokedCallback=\"true\"` enabled, `onBackPressed` (\nand `KEYCODE_BACK`) will no longer be called. In that case, the `AHEAD_OF_TIME` back handling model should be preferred.\n\n## Screens\n\n`FirstScreen` looks like this (assuming you have `data object` enabled):\n\n```groovy\nkotlinOptions {\n    jvmTarget = \"1.8\"\n    languageVersion = '1.9' // data objects, 1.8 in 1.7.21, 1.9 in 1.8.10\n}\n\nkotlin.sourceSets.all {\n    languageSettings.enableLanguageFeature(\"DataObjects\")\n}\n```\n\n```kotlin\n// no args\n@Parcelize\ndata object FirstScreen : DefaultFragmentKey() {\n    override fun instantiateFragment(): Fragment = FirstFragment()\n}\n```\n\nIf you don't have `data object` support yet, then no-args keys look like this (to ensure stable\nhashCode/equals/toString):\n\n``` kotlin\n// no args\n@Parcelize\ndata class FirstScreen(private val noArgsPlaceholder: String = \"\"): DefaultFragmentKey() {\n    override fun instantiateFragment(): Fragment = FirstFragment()\n}\n\n// has args\n@Parcelize\ndata class FirstScreen(\n    val username: String, \n    val password: String,\n): DefaultFragmentKey() {\n    override fun instantiateFragment(): Fragment = FirstFragment()\n}\n```\n\nAnd `FirstFragment` looks like this:\n\n``` kotlin\nclass FirstFragment: KeyedFragment(R.layout.first_fragment) {\n    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {\n        super.onViewCreated(view, savedInstanceState)\n\n        val key: FirstScreen = getKey() // params\n    }\n}\n```\n\nAfter which going to the second screen is as simple as `backstack.goTo(SecondScreen())`.\n\n\n## Scopes\n\nTo simplify sharing data/state between screens, a screen key can implement `ScopeKey`.\n\nThe scope is described with a String tag, and services bound to that scope can be configured via `ScopedServices`.\n\nServices bound to a `ServiceBinder` get lifecycle callbacks: `ScopedServices.Registered`, `ScopedServices.Activated`, or `Bundleable`.\n\nThis lets you easily share a class between screens, while still letting you handle Android's lifecycles seamlessly.\n\nUsing the `simple-stack-extensions`, this can be simplified using the `DefaultServiceProvider`. \n\nIt looks like this:\n\n``` kotlin\nNavigator.configure()\n    .setScopedServices(DefaultServiceProvider())\n    /* ... */\n```\n\nAnd then:\n\n\n``` kotlin\n@Parcelize // typically data class\ndata object FirstScreen: DefaultFragmentKey(), DefaultServiceProvider.HasServices {\n    override fun instantiateFragment(): Fragment = FirstFragment()\n\n    override fun getScopeTag() = toString()\n\n    override fun bindServices(serviceBinder: ServiceBinder) {\n        with(serviceBinder) {\n            add(FirstScopedModel())\n        }\n    }\n}\n\nclass FirstScopedModel : Bundleable, ScopedServices.Registered { // interfaces are optional\n    ...\n}\n\nclass FirstFragment : KeyedFragment(R.layout.first_fragment) {\n    private val firstModel by lazy { lookup\u003cFirstScopedModel\u003e() }\n\n    ...\n}\n\nclass SecondFragment : KeyedFragment(R.layout.second_fragment) {\n    private val firstModel by lazy { lookup\u003cFirstScopedModel\u003e() } // \u003c- available if FirstScreen is in the backstack\n\n    ...\n}\n```\n\nAnd `FirstScopedModel` is shared between two screens.\n\nAny additional shared scopes on top of screen scopes can be defined using `ScopeKey.Child`.\n\n## What are additional benefits?\n\nMaking your navigation state explicit means you're in control of your application.\n\nInstead of hacking around with the right fragment transaction tags, or calling `NEW_TASK | CLEAR_TASK` and making the screen flicker - you can just say `backstack.setHistory(History.of(SomeScreen(), OtherScreen())` and that is now your active navigation history.\n\nUsing `Backstack` to navigate allows you to move navigation responsibilities out of your view layer. No need to run FragmentTransactions directly in a click listener each time you want to move to a different screen. No need to mess around with  `LiveData\u003cEvent\u003cT\u003e\u003e` or `SingleLiveData` to get your \"view\" to decide what state your app should be in either.\n\n``` java\nclass FirstScopedModel(private val backstack: Backstack) {\n    fun doSomething() {\n        // ...\n        backstack.goTo(SecondScreen)\n    }\n}\n```\n\nAnother additional benefit is that your navigation history can be unit tested.\n\n``` java\nassertThat(backstack.getHistory()).containsExactly(SomeScreen, OtherScreen)\n```\n\nAnd most importantly, navigation (swapping screens) happens in one place, and you are in direct control of what happens in such a scenario. By writing a `StateChanger`, you can set up \"how to display my current navigation state\" in any way you want. No more `((MainActivity)getActivity()).setTitleText(\"blah\");` inside Fragment's `onStart()`.\n\nWrite once, works in all cases.\n\n``` java\noverride fun onNavigationEvent(stateChange: StateChange) { // using SimpleStateChanger\n    val newScreen = stateChange.topNewKey\u003cMyScreen\u003e() // use your new navigation state\n\n    setTitle(newScreen.title);\n\n    ... // set up fragments, set up views, whatever you want\n}\n```\n\nWhether you navigate forward or backward, or you rotate the screen, or you come back after low memory condition - it's\nirrelevant. The `StateChanger` will ***always*** handle the scenario in a predictable way.\n\n## Dev Talk about Simple-Stack\n\nFor an overview of the \"why\" and the \"what\" of what Simple-Stack offers, you can check\nout [this talk called `Simplified Single-Activity Apps using Simple-Stack`](https://www.youtube.com/watch?v=5ACcin1Z2HQ)\n.\n\n## Tutorial by Ryan Kay\n\nFor a quick tutorial on how to set up dependency injection, model lifecycles, and reactive state management using\nSimple-Stack, you can look at the tutorial by Ryan Michael Kay [**here, by clicking this\nlink**](https://youtu.be/yRVt6sALB-g?t=2600).)\n\n## More information\n\nFor more information, check the [wiki page](https://github.com/Zhuinden/simple-stack/wiki).\n\n## What about Jetpack Compose?\n\nSee https://github.com/Zhuinden/simple-stack-compose-integration/ for a default way to use composables as screens.\n\nThis however is only required if ONLY composables are used, and NO fragments. When using Fragments, refer to the\nofficial [Fragment Compose interop](https://developer.android.com/jetpack/compose/interop/interop-apis#compose-in-fragments)\nguide.\n\nFor Fragment + Simple-Stack + Compose integration, you can also\ncheck [the corresponding sample](https://github.com/Zhuinden/simple-stack/tree/ced6d11e711fa2dda85e3bd7813cb2a192f10396/samples/advanced-samples/extensions-compose-example)\n.\n\n## About the event-bubbling back handling model\n\nThis section is provided for those who are transitioning from event-bubbling to the ahead-of-time back handling\nmodel (`OnBackPressedDispatcher`), but cannot use the ahead-of-time model yet (due to relying on `onBackPressed()` or `KEYCODE_BACK`).\n\n**Note:** Before supporting predictive back gestures and using `EVENT_BUBBLING` back handling model, the code that interops\nwith OnBackPressedDispatcher looks like this:\n\n``` kotlin\nclass MainActivity : AppCompatActivity(), SimpleStateChanger.NavigationHandler {\n    private lateinit var fragmentStateChanger: DefaultFragmentStateChanger\n\n    @Suppress(\"DEPRECATION\")\n    private val backPressedCallback = object: OnBackPressedCallback(true) {\n        override fun handleOnBackPressed() {\n            if (!Navigator.onBackPressed(this@MainActivity)) {\n                this.remove() \n                onBackPressed() // this is the reliable way to handle back for now \n                this@MainActivity.onBackPressedDispatcher.addCallback(this)\n            }\n        }\n    }\n    \n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        onBackPressedDispatcher.addCallback(backPressedCallback) // this is the reliable way to handle back for now\n\n        val binding = ActivityMainBinding.inflate(layoutInflater)\n        setContentView(binding.root)\n        \n        fragmentStateChanger = DefaultFragmentStateChanger(supportFragmentManager, R.id.container)\n        \n        Navigator.configure()\n            .setStateChanger(SimpleStateChanger(this))\n            .install(this, binding.container, History.single(HomeKey))\n    }\n\n    override fun onNavigationEvent(stateChange: StateChange) {\n        fragmentStateChanger.handleStateChange(stateChange)\n    }\n}\n```\n\nTo handle back previously, what you had to do is override `onBackPressed()` (then\ncall `backstack.goBack()`, if it returned `true` then you would not call `super.onBackPressed()`) , but in order to\nsupport `BackHandler` in Compose, or Fragments that use `OnBackPressedDispatcher` internally, you cannot\noverride `onBackPressed` anymore in a reliable manner.\n\nNow, either this should be used (if cannot migrate to `AHEAD_OF_TIME` back handling model), or migrate\nto `AHEAD_OF_TIME` back handling model and `AheadOfTimeBackCallback` (see example at the start of this readme).\n\n## License\n\n    Copyright 2017-2023 Gabor Varadi\n\n    Licensed under the Apache License, Version 2.0 (the \"License\");\n    you may not use this file except in compliance with the License.\n    You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n    Unless required by applicable law or agreed to in writing, software\n    distributed under the License is distributed on an \"AS IS\" BASIS,\n    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n    See the License for the specific language governing permissions and\n    limitations under the License.\n","funding_links":[],"categories":["Java"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FZhuinden%2Fsimple-stack","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FZhuinden%2Fsimple-stack","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FZhuinden%2Fsimple-stack/lists"}