[ACTIVE] Simple Stack, a backstack library for simpler navigation between views, fragments, or whatevers.
Zhuinden 1.13.1: Add lookupFromScope()/canFindFromScope() (#125)
* Add `canFindFromScope()` and `lookupFromScope()` methods

* 1.13.1: Add lookupFromScope()/canFindFromScope()
Latest commit 3bb349c Nov 25, 2018
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
.idea Add simple-stack-example-scoping to show ScopeKey.Child + LiveData Sep 17, 2018
gradle/wrapper Update build tools Apr 20, 2018
projectFilesBackup/.idea 1.5.0: merge zhuinden/navigator into zhuinden/simple-stack Apr 7, 2017
simple-stack-example-basic-java-fragment Rename some of the example modules Aug 23, 2018
simple-stack-example-basic-java-view Rename some of the example modules Aug 23, 2018
simple-stack-example-basic-kotlin Add simple-stack-example-scoping to show ScopeKey.Child + LiveData Sep 17, 2018
simple-stack-example-conductor Update build tools May 12, 2018
simple-stack-example-kotlin-community-sample Add simple-stack-example-scoping to show ScopeKey.Child + LiveData Sep 17, 2018
simple-stack-example-multistack Update build tools May 12, 2018
simple-stack-example-mvp-fragments Update MVP-Fragment sample to be closer to actual MVP pattern Sep 2, 2018
simple-stack-example-mvp-view Update MVP-View sample to be closer to actual MVP pattern Sep 1, 2018
simple-stack-example-mvvm-fragments Use ScopedServices in MVVM sample to replace VM holder retained frags Sep 2, 2018
simple-stack-example-scoping Use fragment's extension method instead of view's Sep 17, 2018
simple-stack-example-sharedelement-fragment Use `setReorderingAllowed(true)` for show/hide shared element transition Jul 22, 2018
simple-stack 1.13.1: Add lookupFromScope()/canFindFromScope() (#125) Nov 25, 2018
.gitignore Update build tools May 12, 2018
CHANGELOG.md 1.13.1: Add lookupFromScope()/canFindFromScope() (#125) Nov 25, 2018
LICENSE.txt added a bunch of license stuff Feb 10, 2017
README.md 1.13.1: Add lookupFromScope()/canFindFromScope() (#125) Nov 25, 2018
build.gradle First draft of scoping system Jul 1, 2018
circle.yml Update circle.yml Feb 25, 2017
gradle.properties added basic-kotlin example Jun 7, 2017
gradlew Update Gradle wrapper to 4.3.1 Nov 9, 2017
gradlew.bat Update Gradle wrapper to 4.3.1 Nov 9, 2017
settings.gradle Add simple-stack-example-scoping to show ScopeKey.Child + LiveData Sep 17, 2018

README.md

Simple Stack

Similarly to square/flow, Simple Stack allows you to represent your application state in a list of immutable data classes.

The library also allows easy backstack persisting through a delegate class, which handles configuration changes and process death.

If your data classes are not Parcelable by default, then you can specify a custom parcellation strategy using setKeyParceler().

Additionally, the library also allows you to persist state of custom viewgroups that are associated with a given UI state into a StateBundle.

This way, you can easily create a single-Activity application using either views, fragments, or whatevers.

Operators

The Backstack provides 3 primary operators for manipulating state.

  • goTo(): if state does not previously exist in the backstack, then adds it to the stack. Otherwise navigate back to given state.
  • goBack(): returns boolean if StateChange is in progress, or if there are more than 1 entries in history (and handled the back press). Otherwise, return false.
  • setHistory(): sets the state to the provided elements, with the direction that is specified.

The secondary operators are:

  • replaceTop(): removes the current top element, and replaces it with the newly provided one.
  • goUp(): navigates back to the element if exists, replaces current top with it if does not.
  • goUpChain(): goes up to the parent chain if exists completely, replaces current with the chain if partially exists (while re-ordering existing duplicates to match the provided chain), and replaces current with chain if doesn't exist.
  • jumpToRoot(): goes to the root of the stack with the given direction (by default, backwards).
  • moveToTop(): moves to provided key to the top if exists, otherwise adds it to top.

What does it do?

The Backstack stores the screens.

The Backstack also allows navigation between the states (works as a router), and enables handling this state change using the StateChanger.

The library also provides two ways to handle both view-state persistence for views associated with a key, and persisting the keys across configuration change / process death.

  • The Navigator, which uses the BackstackHost retained fragment (API 11+) to automatically receive the lifecycle callbacks, and survive configuration change.

  • The BackstackDelegate, which works via manual Activity lifecycle callbacks - typically needed only for fragments.

Internally, both the BackstackDelegate and the Navigator uses a BackstackManager, which can also be used.


The library provides a DefaultStateChanger, which by default uses Navigator to handle the persistence.

The keys used by a DefaultStateChanger must implement StateKey, which expects a layout key and a view change handler.

Using Simple Stack

In order to use Simple Stack, you need to add jitpack to your project root gradle:

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

and add the compile dependency to your module level gradle.

compile 'com.github.Zhuinden:simple-stack:1.13.1'

How does it work?

The Backstack must be initialized with at least one initial state, and a StateChanger must be set when it is able to handle the state change.

The BackstackManager is provided to handle state persistence.

Convenience classes BackstackDelegate and Navigator are provided to help integration of the BackstackManager.

Setting a StateChanger begins an initialization (in Flow terms, a bootstrap traversal), which provides a StateChange in form of {[], [{...}, {...}]} (meaning the previous state is empty, the new state is the initial keys).

This allows you to initialize your views according to your current state.

Afterwards, the Backstack operators allow changing between states.

Example code

Fragments

Check out the Fragment samples to see how to make Simple-Stack work with Fragments (or the relevant wiki page).

  • End result
    fun navigateTo(key: Key) {
        backstack.goTo(key)
    }

and

    homeButton.onClick {
        navigateTo(OtherKey())
    }
  • Activity
class MainActivity : AppCompatActivity(), StateChanger {
    lateinit var backstackDelegate: BackstackDelegate
    lateinit var fragmentStateChanger: FragmentStateChanger

    override fun onCreate(savedInstanceState: Bundle?) {
        backstackDelegate = BackstackDelegate()
        backstackDelegate.onCreate(savedInstanceState,
            lastCustomNonConfigurationInstance,
            History.single(TasksKey()))

        backstackDelegate.registerForLifecycleCallbacks(this)

        super.onCreate(savedInstanceState)

        setContentView(R.layout.activity_main)

        this.fragmentStateChanger = FragmentStateChanger(supportFragmentManager, R.id.root)
        
        backstackDelegate.setStateChanger(this)
    }
    override fun onRetainCustomNonConfigurationInstance(): Any =
        backstackDelegate.onRetainCustomNonConfigurationInstance()

    override fun onBackPressed() {
        if (!backstackDelegate.onBackPressed()) { // calls backstack.goBack()
            super.onBackPressed()
        }
    }

    override fun handleStateChange(stateChange: StateChange, completionCallback: StateChanger.Callback) {
        if (stateChange.topNewState<Any>() == stateChange.topPreviousState()) {
            // no-op
            completionCallback.stateChangeComplete()
            return
        }

        fragmentStateChanger.handleStateChange(stateChange)

        completionCallback.stateChangeComplete()
    }
}
  • Key for Fragments
@Parcelize
data class TasksKey(val placeholder: String = "") : BaseKey() {
    override fun createFragment(): Fragment = TasksFragment()
}

And

abstract class BaseKey : Parcelable {
    val fragmentTag: String
        get() = toString()

    fun newFragment(): BaseFragment = createFragment().apply {
        arguments = (arguments ?: Bundle()).also { bundle ->
            bundle.putParcelable("KEY", this@BaseKey)
        }
    }

    protected abstract fun createFragment(): BaseFragment
}
  • FragmentStateChanger

For FragmentStateChanger, see the example here.

Custom Views

  • Activity
class MainActivity : AppCompatActivity() {

    @Override
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        Navigator.install(this, root, History.single(FirstKey()))
        // additional configuration possible with `Navigator.configure()...install()`
    }

    @Override
    override fun onBackPressed() {
        if(!Navigator.onBackPressed(this)) { // calls `backstack.goBack()`
            super.onBackPressed()
        }
    }
}
  • StateKey
@Parcelize
data class TasksKey(val placeholder: String = "") : StateKey {
    override fun layout(): Int = R.layout.path_tasks
    override fun viewChangeHandler(): ViewChangeHandler = SegueViewChangeHandler()
}
  • Layout XML
<?xml version="1.0" encoding="utf-8"?>
<com.zhuinden.simplestackdemoexample.FirstView xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:gravity="center"
              android:orientation="vertical">

    <EditText
        android:id="@+id/first_edittext"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:hint="Enter text here"/>

    <Button
        android:id="@+id/first_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Go to second!"/>

</com.zhuinden.simplestackdemoexample.FirstView>
  • Custom Viewgroup
class TasksView : FrameLayout {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
    constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
    @TargetApi(21)
    constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes)
    
    override fun onFinishInflate() {
         super.onFinishInflate()
         firstButton.onClick {
             backstack.goTo(SeocndKey())
         }
    }
}

val View.backstack 
    get() = Navigator.getBackstack(context)

More information

For more information, check the wiki page.

License

Copyright 2017-2018 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.