• Stars
    star
    1,319
  • Rank 35,659 (Top 0.8 %)
  • Language
    Java
  • License
    Apache License 2.0
  • Created almost 8 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

[ACTIVE] Simple Stack, a backstack library / navigation framework for simpler navigation and state management (for fragments, views, or whatevers).

featured License

simple-stack

Simple Stack

Why do I want this?

To make navigation to another screen as simple as backstack.goTo(SomeScreen()), and going back as simple as backstack.goBack().

No more FragmentTransactions in random places. Predictable and customizable navigation in a single location.

What is Simple Stack?

Simple 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").

This allows preserving your navigation history across configuration changes and process death - this is handled automatically.

Each screen can be associated with a scope, or a shared scope - to easily share data between screens.

This simplifies navigation and state management within an Activity using either fragments, views, or whatever else.

Using Simple Stack

In order to use Simple Stack, you need to add jitpack to your project root build.gradle.kts (or build.gradle):

// build.gradle.kts
allprojects {
    repositories {
        // ...
        maven { setUrl("https://jitpack.io") }
    }
    // ...
}

or

// build.gradle
allprojects {
    repositories {
        // ...
        maven { url "https://jitpack.io" }
    }
    // ...
}

In newer projects, you need to also update the settings.gradle file's dependencyResolutionManagement block:

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
        maven { url 'https://jitpack.io' }  // <--
        jcenter() // Warning: this repository is going to shut down soon
    }
}

and then, add the dependency to your module's build.gradle.kts (or build.gradle):

// build.gradle.kts
implementation("com.github.Zhuinden:simple-stack:2.8.0")
implementation("com.github.Zhuinden:simple-stack-extensions:2.3.3")

or

// build.gradle
implementation 'com.github.Zhuinden:simple-stack:2.8.0'
implementation 'com.github.Zhuinden:simple-stack-extensions:2.3.3'

How do I use it?

You can check out the tutorials for simple examples.

Fragments

With Fragments, in AHEAD_OF_TIME back handling mode to support predictive back gesture (along with android:enableBackInvokedCallback), the Activity code looks like this:

  • With simple-stack-extensions:lifecycle-ktx
class MainActivity : AppCompatActivity(), SimpleStateChanger.NavigationHandler {
    private lateinit var fragmentStateChanger: FragmentStateChanger
    private lateinit var backstack: Backstack

    private val backPressedCallback = object : OnBackPressedCallback(false) { // <-- !
        override fun handleOnBackPressed() {
            backstack.goBack()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.main_activity)

        onBackPressedDispatcher.addCallback(backPressedCallback) // <-- !

        fragmentStateChanger = FragmentStateChanger(supportFragmentManager, R.id.container)

        backstack = Navigator.configure()
            .setBackHandlingModel(BackHandlingModel.AHEAD_OF_TIME) // <-- !
            .setStateChanger(SimpleStateChanger(this))
            .install(this, binding.container, History.single(HomeKey))

        backPressedCallback.isEnabled = backstack.willHandleAheadOfTimeBack() // <-- !
        backstack.observeAheadOfTimeWillHandleBackChanged(this, backPressedCallback::isEnabled::set) // <-- ! from lifecycle-ktx
    }
    
    override fun onNavigationEvent(stateChange: StateChange) {
        fragmentStateChanger.handleStateChange(stateChange)
    }
}
  • Without simple-stack-extensions:lifecycle-ktx
class MainActivity : AppCompatActivity(), SimpleStateChanger.NavigationHandler {
    private lateinit var fragmentStateChanger: FragmentStateChanger
    private lateinit var backstack: Backstack

    private val backPressedCallback = object : OnBackPressedCallback(false) { // <-- !
        override fun handleOnBackPressed() {
            backstack.goBack()
        }
    }

    private val updateBackPressedCallback = AheadOfTimeWillHandleBackChangedListener { // <-- !
        backPressedCallback.isEnabled = it // <-- !
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.main_activity)

        onBackPressedDispatcher.addCallback(backPressedCallback) // <-- !

        fragmentStateChanger = FragmentStateChanger(supportFragmentManager, R.id.container)

        backstack = Navigator.configure()
            .setBackHandlingModel(BackHandlingModel.AHEAD_OF_TIME) // <-- !
            .setStateChanger(SimpleStateChanger(this))
            .install(this, binding.container, History.single(HomeKey))

        backPressedCallback.isEnabled = backstack.willHandleAheadOfTimeBack() // <-- !
        backstack.addAheadOfTimeWillHandleBackChangedListener(updateBackPressedCallback) // <-- !
    }

    override fun onDestroy() {
        backstack.removeAheadOfTimeWillHandleBackChangedListener(updateBackPressedCallback); // <-- !
        super.onDestroy()
    }

    override fun onNavigationEvent(stateChange: StateChange) {
        fragmentStateChanger.handleStateChange(stateChange)
    }
}

With targetSdkVersion 34 and with android:enableOnBackInvokedCallback="true" enabled, onBackPressed ( and KEYCODE_BACK) will no longer be called. In that case, the AHEAD_OF_TIME back handling model should be preferred.

Screens

FirstScreen looks like this (assuming you have data object enabled):

kotlinOptions {
    jvmTarget = "1.8"
    languageVersion = '1.9' // data objects, 1.8 in 1.7.21, 1.9 in 1.8.10
}

kotlin.sourceSets.all {
    languageSettings.enableLanguageFeature("DataObjects")
}
// no args
@Parcelize
data object FirstScreen : DefaultFragmentKey() {
    override fun instantiateFragment(): Fragment = FirstFragment()
}

If you don't have data object support yet, then no-args keys look like this (to ensure stable hashCode/equals/toString):

// no args
@Parcelize
data class FirstScreen(private val noArgsPlaceholder: String = ""): DefaultFragmentKey() {
    override fun instantiateFragment(): Fragment = FirstFragment()
}

// has args
@Parcelize
data class FirstScreen(
    val username: String, 
    val password: String,
): DefaultFragmentKey() {
    override fun instantiateFragment(): Fragment = FirstFragment()
}

And FirstFragment looks like this:

class FirstFragment: KeyedFragment(R.layout.first_fragment) {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val key: FirstScreen = getKey() // params
    }
}

After which going to the second screen is as simple as backstack.goTo(SecondScreen()).

Scopes

To simplify sharing data/state between screens, a screen key can implement ScopeKey.

The scope is described with a String tag, and services bound to that scope can be configured via ScopedServices.

Services bound to a ServiceBinder get lifecycle callbacks: ScopedServices.Registered, ScopedServices.Activated, or Bundleable.

This lets you easily share a class between screens, while still letting you handle Android's lifecycles seamlessly.

Using the simple-stack-extensions, this can be simplified using the DefaultServiceProvider.

It looks like this:

Navigator.configure()
    .setScopedServices(DefaultServiceProvider())
    /* ... */

And then:

@Parcelize // typically data class
data object FirstScreen: DefaultFragmentKey(), DefaultServiceProvider.HasServices {
    override fun instantiateFragment(): Fragment = FirstFragment()

    override fun getScopeTag() = toString()

    override fun bindServices(serviceBinder: ServiceBinder) {
        with(serviceBinder) {
            add(FirstScopedModel())
        }
    }
}

class FirstScopedModel : Bundleable, ScopedServices.Registered { // interfaces are optional
    ...
}

class FirstFragment : KeyedFragment(R.layout.first_fragment) {
    private val firstModel by lazy { lookup<FirstScopedModel>() }

    ...
}

class SecondFragment : KeyedFragment(R.layout.second_fragment) {
    private val firstModel by lazy { lookup<FirstScopedModel>() } // <- available if FirstScreen is in the backstack

    ...
}

And FirstScopedModel is shared between two screens.

Any additional shared scopes on top of screen scopes can be defined using ScopeKey.Child.

What are additional benefits?

Making your navigation state explicit means you're in control of your application.

Instead 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.

Using 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<Event<T>> or SingleLiveData to get your "view" to decide what state your app should be in either.

class FirstScopedModel(private val backstack: Backstack) {
    fun doSomething() {
        // ...
        backstack.goTo(SecondScreen)
    }
}

Another additional benefit is that your navigation history can be unit tested.

assertThat(backstack.getHistory()).containsExactly(SomeScreen, OtherScreen)

And 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().

Write once, works in all cases.

override fun onNavigationEvent(stateChange: StateChange) { // using SimpleStateChanger
    val newScreen = stateChange.topNewKey<MyScreen>() // use your new navigation state

    setTitle(newScreen.title);

    ... // set up fragments, set up views, whatever you want
}

Whether you navigate forward or backward, or you rotate the screen, or you come back after low memory condition - it's irrelevant. The StateChanger will always handle the scenario in a predictable way.

Dev Talk about Simple-Stack

For an overview of the "why" and the "what" of what Simple-Stack offers, you can check out this talk called Simplified Single-Activity Apps using Simple-Stack .

Tutorial by Ryan Kay

For a quick tutorial on how to set up dependency injection, model lifecycles, and reactive state management using Simple-Stack, you can look at the tutorial by Ryan Michael Kay here, by clicking this link.)

More information

For more information, check the wiki page.

What about Jetpack Compose?

See https://github.com/Zhuinden/simple-stack-compose-integration/ for a default way to use composables as screens.

This however is only required if ONLY composables are used, and NO fragments. When using Fragments, refer to the official Fragment Compose interop guide.

For Fragment + Simple-Stack + Compose integration, you can also check the corresponding sample .

About the event-bubbling back handling model

This section is provided for those who are transitioning from event-bubbling to the ahead-of-time back handling model (OnBackPressedDispatcher), but cannot use the ahead-of-time model yet (due to relying on onBackPressed() or KEYCODE_BACK).

Note: Before supporting predictive back gestures and using EVENT_BUBBLING back handling model, the code that interops with OnBackPressedDispatcher looks like this:

class MainActivity : AppCompatActivity(), SimpleStateChanger.NavigationHandler {
    private lateinit var fragmentStateChanger: DefaultFragmentStateChanger

    @Suppress("DEPRECATION")
    private val backPressedCallback = object: OnBackPressedCallback(true) {
        override fun handleOnBackPressed() {
            if (!Navigator.onBackPressed(this@MainActivity)) {
                this.remove() 
                onBackPressed() // this is the reliable way to handle back for now 
                this@MainActivity.onBackPressedDispatcher.addCallback(this)
            }
        }
    }
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        onBackPressedDispatcher.addCallback(backPressedCallback) // this is the reliable way to handle back for now

        val binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
        fragmentStateChanger = DefaultFragmentStateChanger(supportFragmentManager, R.id.container)
        
        Navigator.configure()
            .setStateChanger(SimpleStateChanger(this))
            .install(this, binding.container, History.single(HomeKey))
    }

    override fun onNavigationEvent(stateChange: StateChange) {
        fragmentStateChanger.handleStateChange(stateChange)
    }
}

To handle back previously, what you had to do is override onBackPressed() (then call backstack.goBack(), if it returned true then you would not call super.onBackPressed()) , but in order to support BackHandler in Compose, or Fragments that use OnBackPressedDispatcher internally, you cannot override onBackPressed anymore in a reliable manner.

Now, either this should be used (if cannot migrate to AHEAD_OF_TIME back handling model), or migrate to AHEAD_OF_TIME back handling model and AheadOfTimeBackCallback (see example at the start of this readme).

License

Copyright 2017-2023 Gabor Varadi

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

   http://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.

More Repositories

1

guide-to-kotlin

[GUIDE] This tutorial assumes all you know is Java, but you want to learn Kotlin.
1,288
star
2

jetpack-navigation-ftue-sample

[DEMO] Sample code to display "First-Time User Experience" in a Single-Activity app using Jetpack-Navigation, NavGraphs, Dagger, SavedStateHandle, Hilt, and EventEmitter - based on the FTUE example code in simple-stack-tutorials, but originally described by Google.
Kotlin
236
star
3

fragmentviewbindingdelegate-kt

[ACTIVE] A delegate for making managing the ViewBinding variable in a Fragment simpler.
Kotlin
151
star
4

flowless

[DEPRECATED] Based on Flow 1.0-alpha. To keep your life simple, use zhuinden/simple-stack instead.
Java
141
star
5

espresso-helper

[STAGNANT?] Collection of Kotlin helpers for Espresso.
Kotlin
131
star
6

simple-stack-compose-integration

[ACTIVE/BETA] Compose integration for Simple-Stack.
Kotlin
98
star
7

realm-monarchy

[ACTIVE-ISH] A wrapper over Realm which exposes it as LiveData, managing Realm lifecycle internally.
Java
86
star
8

livedata-combinetuple-kt

[ACTIVE] Helper function to combine LiveData into tuples.
Kotlin
85
star
9

realm-book-example

This is an example rewrite of AndroidHive's messy tutorial, accompanying the following article on Realm.
Java
80
star
10

event-emitter

[ACTIVE] The event emitter allows you to register multiple observers, but enqueue events while there are no observers.
Java
76
star
11

live-event

[ACTIVE] Lifecycle-aware wrapper over EventEmitter, for modelling one-off events.
Kotlin
62
star
12

mvvm-aac-rxjava-retrofit-room

[DEMO] MVVM, AAC (ViewModel), RxJava2, Retrofit, Room
Kotlin
54
star
13

BottomNavChildFragmentExample

An example showing how to use bottom navigation with child fragments.
Kotlin
44
star
14

Jetpack-Navigation-Hilt-MultiModule-Example

A simple example showing multi-module navigation with safeargs and jetpack navigation.
Kotlin
42
star
15

ViewBindingExample

An example of using ViewBinding in Activity/Fragment.
Kotlin
37
star
16

Jetpack-Navigation-Multistack-Example

[DEMO] An example using Jetpack Navigation and bottom nav multi-stack using child fragments.
Kotlin
36
star
17

service-tree

[ABANDONED] A tree that stores services in its node for a given key, and allows traversing them.
Java
33
star
18

flow-combinetuple-kt

[ACTIVE] Helper function to combine Flow into tuples.
Kotlin
32
star
19

simple-stack-tutorials

[MOVED] Guide was merged into `simple-stack` repository.
Kotlin
30
star
20

xkcd-example

[SIMPLE DEMO] A super-simple no-architecture app with Retrofit, Realm, and Glide.
Java
29
star
21

state-bundle

[ACTIVE] A non-Android Parcelable replacement for Bundle.
Java
28
star
22

simple-stack-ftue-sample

[DEMO] Sample code to display "First-Time User Experience" in a Single-Activity app using Simple-Stack, based on the "Conditional Navigation" section by Google.
Kotlin
27
star
23

realm-helpers

[ABANDONED] A collection of helpers that are still all in an early stage, but some people could consider them helpful.
Java
23
star
24

MortarFlowSetup

OBSOLETE: USE `simple-stack` instead!
Java
22
star
25

sync-timer-app

[APP] Sync Timer allows multiple people to join, and see a shared countdown. It stops if someone stops it.
Java
20
star
26

DaggerViewModelExperiment

[DEPRECATED, EXPERIMENT] This is a PoC example for app->activity->fragment subscoping with Subcomponents + ViewModel by SavedStateHandle (using AutoFactory). You shouldn't use this setup, because it breaks scoping. Refer to https://github.com/Zhuinden/jetpack-navigation-ftue-sample for a proper setup.
Kotlin
19
star
27

jetpack-navigation-ftue-compose-sample

[DEMO] FTUE sample using Jetpack Navigation's Navigation-Compose, ViewModel, SavedStateHandle, Hilt
Kotlin
18
star
28

command-queue

[ACTIVE] A queue with a single receiver and if there is no receiver, the commands are enqueued.
Java
17
star
29

android-dev-challenge-compose-design

Kotlin
17
star
30

room-live-paged-list-provider-experiment

An experiment with using Room and LivePagedListProvider.
Java
16
star
31

navigator

[RELOCATED] The contents of this repository were merged into simple-stack 1.5.0.
Java
15
star
32

tuples-kt

[ACTIVE] Tuples from 4 to 16 arity in Kotlin.
Kotlin
15
star
33

realm-auto-migration

[ABANDONED] Automatic migration from the currently existing schema to the currently existing model classes.
Java
15
star
34

flow-ziptuple-kt

[ACTIVE] Helper functions to zip Flows into 3 to 11 arity tuples, and to array.
Kotlin
12
star
35

single-activity-instant-app-example

This is an experiment to set up a "single-activity" app in an instant-app setup.
Kotlin
10
star
36

compose-adopt-a-dog

Submission for #AndroidDevChallenge Week 1.
Kotlin
10
star
37

singleton-realm-manager

[MOVED] Moved to Realm-Helpers. The RealmManager class allows opening/closing the Realm instance, but also obtaining it without incrementing the cache's reference count.
Java
9
star
38

livedata-combineutil-java

[ACTIVE] Helper function to combine LiveDatas.
Java
9
star
39

rx-combinetuple-kt

[ACTIVE] Helper function to combine RxJava observables into tuples.
Kotlin
8
star
40

realm-databind-experiment

Making Realm work with Databinding.
Java
8
star
41

android-dev-challenge-compose-clock

Kotlin
7
star
42

rx-realm-recyclerview-experiment

Checking out RX with RecyclerView and Realm.
Java
6
star
43

MortarFlowInitialDemo

OBSOLETE: USE `Flowless` instead!
Java
6
star
44

compose-simple-stack-experiment

Initial experiment with Compose + Simple-Stack.
Kotlin
6
star
45

livedata-validateby-kt

[ACTIVE] Helper functions to combine multiple boolean streams into a single boolean.
Kotlin
6
star
46

flow-validateby-kt

[ACTIVE] Helper function for Flow, to combine multiple boolean values into a single boolean.
Kotlin
5
star
47

simple-stack-extensions

Extensions for the simple-stack library.
Kotlin
4
star
48

ExampleGithubClient

[DEPRECATED] This is an MVP example for Flowless, using Dagger2 subscoping and RxJava Single.
Java
4
star
49

simple-stack-ftue-compose-sample

[DEMO] Simple-stack FTUE sample using Compose integration (and Rx)
Kotlin
3
star
50

rx-validateby-kt

[ACTIVE] Helper to combine multiple boolean streams for simple validation.
Kotlin
3
star
51

navigation-example

Navigation example using simple-stack.
Java
3
star
52

simple-stack-multi-module-experiment

[EXPERIMENT] Experiment with simple-stack + Views in a multi-module setting using Dagger.
Kotlin
3
star
53

AndroidDiceGame

Just a very simple sample project based on a Verilog homework I had a while ago, for experimentations.
Kotlin
2
star
54

scope-manager

[OBSOLETE] This will never happen.
Java
2
star
55

AndroidReactor

AndroidReactor is a framework for a reactive and unidirectional Android application architecture.
Kotlin
2
star
56

FirstTestingApp

This is a repository in which I'm trying to get testing to work. Not much here yet.
Java
2
star
57

realm-samples

[TODO] Realm sample codes for tutorials (currently in production). Check back later!
Kotlin
1
star
58

ButtonShadowGravityProblem

This is so that Reddit can look at it and see how changing the gravity breaks the line at the bottom of the button. SPOILERS: The answer was `scrollX=594742`.
Kotlin
1
star
59

angular-first-app

First app with Angular4, following a tutorial and stuff.
TypeScript
1
star
60

vanilla-cat-example

An example without Dagger2, but with Retrofit and raw android SQLite. Not a good example, though. Too much hackery.
Java
1
star
61

validationk

[NONSENSE] ValidationK: A library that I'm not sure is needed by anyone (from the future: no), but it lets you chain predicates.
Kotlin
1
star
62

flow-sample

Flow-Sample by Square
Java
1
star
63

CasterIO-Simple-Stack

Sample code for Caster.io's Simple-Stack samples.
Kotlin
1
star