- ADDED:
Backstack.goAppendChain(newKeys)
andBackstack.goAppendChain(asReplace, newKeys)
which appends the provided keys to the end of the current history.
If any duplicates are provided, they will also be added to the end. If a key already exists in the history, it gets removed from earlier and appended to the end.
If there are implicit parents used as the reorder occurs, please make sure that the scope hierarchy can still be rebuilt from left-to-right
order. It might be preferred top use ScopeKey.Child
instead of ScopeKey
in these cases.
- ADDED:
Backstack.findServices(serviceSearchMode)
andBackstack.findServicesFromScope(scopeTag, serviceSearchMode)
.
This allows for getting the services of a backstack (either only local services, or including parent services) that are accessible within the backstack.
- ADDED:
Backstack.setParentServices(Backstack parentServices)
,Backstack.setParentServices(Backstack parentServices, String parentScopeTag)
andBackstack.getParentServices()
(as per #239).
When using backstack.lookupService()
, backstack.canFindService()
, backstack.canFindFromScope()
and backstack.lookupFromScope()
, then
if parent services are set, it will attempt to lookup the service with ALL from either the full scope hierarchy, or from the scope provided
as the parentScopeTag
.
Please note that findScopesForKey()
is NOT affected, as it would drastically alter behavior. If you need this, you can collect it from the
parent manually (which is partly why getParentServices()
was added).
-
MAJOR FEATURE ADDITION: Added
Backstack.setBackHandlingModel(BackHandlingModel.AHEAD_OF_TIME)
to supportandroid:enableBackInvokedCallback="true"
on Android 14 for predictive back gesture support. -
This also comes with a new artifact in
simple-stack-extensions 2.3.0
calledlifecycle-ktx
.
With this, Navigator.Installer.setBackHandlingModel()
, BackstackDelegate.setBackHandlingModel()
,
and Backstack.setBackHandlingModel()
are added.
Also, ServiceBinder.getAheadOfTimeBackCallbackRegistry()
is added as a replacement for ScopedServices.HandlesBack
.
Please note that using it requires AHEAD_OF_TIME
mode, and without it, trying to
use ServiceBinder.getAheadOfTimeBackCallbackRegistry()
throws an exception.
Also, Backstack.willHandleAheadOfTimeBack()
, Backstack.addAheadOfTimeWillHandleBackChangedListener()
and Backstack.removeAheadOfTimeWillHandleBackChangedListener()
are added.
IMPORTANT:
The AHEAD_OF_TIME
back handling model must be enabled similarly to how setScopedServices()
or other similar configs
must be called before backstack.setup()
, Navigator.install()
, or BackstackDelegate.onCreate()
.
When AHEAD_OF_TIME
is set, the behavior of goBack()
changes. Calling goBack()
when willHandleAheadOfTimeBack()
returns false throws an exception.
When AHEAD_OF_TIME
is set, ScopedServices.HandlesBack
will no longer be called (as it cannot return whether a
service WILL handle back or not), and should be replaced with registrations to the AheadOfTimeBackCallbackRegistry
.
When AHEAD_OF_TIME
is NOT set (and therefore the default, EVENT_BUBBLING
is set),
calling willHandleAheadOfTimeBack
or addAheadOfTimeWillHandleBackChangedListener
or removeAheadOfTimeWillHandleBackChangedListener
throws an exception.
To migrate to use the ahead-of-time back handling model, then you might have the previous
somewhat onBackPressedDispatcher
-compatible (but not predictive-back-gesture compatible) code:
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)
}
}
This code changes to the following in order to support predictive back gesture using ahead-of-time model:
class MainActivity : AppCompatActivity(), SimpleStateChanger.NavigationHandler {
private lateinit var fragmentStateChanger: FragmentStateChanger
private lateinit var authenticationManager: AuthenticationManager
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)
}
}
Please make sure to remove the AheadOfTimeWillHandleBackChangedListener
in onDestroy
(Activity) or onDestroyView
(
Fragment), because the listener staying registered would be a memory leak.
With the new lifecycle-ktx
in simple-stack-extensions
2.3.0, this:
backPressedCallback.isEnabled = backstack.willHandleAheadOfTimeBack() // <-- !
backstack.addAheadOfTimeWillHandleBackChangedListener(updateBackPressedCallback) // <-- !
}
override fun onDestroy() {
backstack.removeAheadOfTimeWillHandleBackChangedListener(updateBackPressedCallback); // <-- !
can be turned into this:
backPressedCallback.isEnabled = backstack.willHandleAheadOfTimeBack() // <-- !
backstack.observeAheadOfTimeWillHandleBackChanged(this, backPressedCallback::isEnabled::set)
If you can't update to the AHEAD_OF_TIME
back handling model, then don't worry, as backwards compatibility has been
preserved with the previous behavior.
When using AHEAD_OF_TIME
back handling model, ScopedServices.HandlesBack
is no longer called. To replace this, you
might have had something like this:
class FragmentStackHost(
initialKey: Any
) : Bundleable, ScopedServices.HandlesBack {
var isActiveForBack: Boolean = false
// ...
override fun onBackEvent(): Boolean {
if (isActiveForBack) {
return backstack.goBack()
} else {
return false
}
}
}
This is replaced like so:
class FragmentStackHost(
initialKey: Any,
private val aheadOfTimeBackCallbackRegistry: AheadOfTimeBackCallbackRegistry,
) : Bundleable, ScopedServices.Registered {
var isActiveForBack: Boolean = false
set(value) {
field = value
backCallback.isEnabled = value && backstackWillHandleBack
}
private var backstackWillHandleBack = false
set(value) {
field = value
backCallback.isEnabled = isActiveForBack && value
}
private val backCallback = object : AheadOfTimeBackCallback(false) {
override fun onBackReceived() {
backstack.goBack()
}
}
private val willHandleBackChangedListener = AheadOfTimeWillHandleBackChangedListener {
backstackWillHandleBack = it
}
init {
// ...
backstackWillHandleBack = backstack.willHandleAheadOfTimeBack()
backstack.addAheadOfTimeWillHandleBackChangedListener(willHandleBackChangedListener)
}
override fun onServiceRegistered() {
aheadOfTimeBackCallbackRegistry.registerAheadOfTimeBackCallback(backCallback)
}
override fun onServiceUnregistered() {
aheadOfTimeBackCallbackRegistry.unregisterAheadOfTimeCallback(backCallback)
}
}
Where FragmentStackHost
gets the AheadOfTimeBackCallbackRegistry
from serviceBinder.getAheadOfTimeBackCallbackRegistry()
.
So in this snippet, whether back will be handled needs to be propagated up, and manage the enabled state of
the AheadOfTimeBackCallback
to intercept back if needed.
While this might seem a bit tricky, this is how Google does it in their own micromanagement of communicating with
the onBackPressedDispatcher
as well, so evaluating ahead of time who will want to handle back later is unavoidable.
- DEPRECATED:
BackstackDelegate.onBackPressed()
andNavigator.onBackPressed()
. Not only are they the same asbackstack.goBack()
and merely managed to confuse people historically, but this deprecation mirros the deprecation ofonBackPressed
in compileSdk 33, to push towards using predictive back.
-
FIX:
Backstack.CompletionListener
added toBackstack
that unregistered themselves during dispatching notifications would cause either a ConcurrentModificationException or invalid results, this is now fixed and no longer the case ( #263, thanks @angusholder) -
MINOR CHANGE: When
Backstack.CompletionListener
's are being notified, the state changer is temporarily removed ( similarly to dispatchingScopedServices.Activated
events), so that navigation actions invoked onBackstack
are deferred until allBackstack.CompletionListener
s are notified.
-
FIX: Attempt at fixing a crash related to
LinkedHashMap.retainAll()
specifically on Android 6 and Android 6.1 devices (#256). -
2.6.3 had an issue with
maven-publish
and transitive dependencies were missing, and is therefore skipped.
- ADDED:
Backstack.canSetScopeProviders()
.
This is in conjunction with the 2.6.1 change, while making it safe to use them without extra checks such as if(lastNonConfigurationInstance == null) {
.
- CHANGE:
Backstack.setScopedServices(ScopedServices)
,Backstack.setGlobalServices(GlobalServices)
, andBackstack.setGlobalServices(GlobalServices.Factory)
can now be called aftersetup()
, but beforesetStateChanger()
.
This allows setting the scoped services on the backstack instance, when using deferred initialization, before the initial state change is run.
- ADD:
Backstack.addRetainedObject(objectTag, retainedObject)
,Backstack.hasRetainedObject(objectTag)
,Backstack.removeRetainedObject(objectTag)
,Backstack.getRetainedObject(objectTag)
.
This allows simpler way of persisting an object instance across configuration changes.
Also, retained objects that implement Bundleable
are given state restoration callbacks.
-
UPDATE: Add
simple-stack-example-multistack-nested-fragment
that shows how to create a fragment that hasBackstack
s for its child fragments, thus creating true multi-stack apps using nested backstacks. -
DEPRECATED:
Backstack.addCompletionListener
,Backstack.removeCompletionListener
,Backstack.removeCompletionListeners
.
These were the same as addStateChangeCompletionListener
and removeStateChangeCompletionListener
, and should not have been duplicate APIs.
- ADD:
Backstack.exitScope(scopeTag)
,Backstack.exitScope(scopeTag, direction)
andBackstack.exitScopeTo(scopeTag, targetKey, direction)
.
If a scope is found, the backstack now allows exiting from it. Providing a target allows exiting into a new target key.
- ADD:
AsyncStateChanger
for convenience.
Mirroring the addition of SimpleStateChanger
for synchronous state changes, AsyncStateChanger
is for async state changes (while still no longer having to remember checking for the same key being provided using isTopNewKeyEqualToPrevious
).
- UPDATE:
state-bundle
is updated to1.4.0
(add a few missing@Nullable
s that became platform types instead of nullables).
- SIGNATURE CHANGE:
GlobalServices.Factory
now receivesBackstack
parameter increate()
. (#231)
I'm aware this is technically "breaking", but the effect should be minor, and hopefully shouldn't cause problems.`
The Backstack
cannot be added as a service
directly, but it can be added as an alias
.
-
FIX:
GlobalServices.Factory
'screate()
method was non-null, but@Nonnull
was missing. -
MINOR FIX: Adding the
Backstack
fromserviceBinder.getBackstack()
withaddService()
would cause a loop intoBundle()
. Now it explicitly throwsIllegalArgumentException
instead sooner (not reported before). -
DEPRECATED:
backstack.removeAllStateChangeCompletionListeners()
. This was added "for convenience", but in reality it is not a good/safe API, and it should not exist. -
UPDATE: Create and release
simple-stack-extensions
for default scoping and default fragment behaviors. -
ADD:
GlobalServices.SCOPE_TAG
to make it possible to see the scope tag of global services without relying on internals.
- FIX: Bug introduced in 2.3.1, using
backstack.removeAllStateChangeCompletionListeners()
would remove an internal completion listener and therefore scope activation callbacks would stop being dispatched.
- FIX: Ensure that if multiple navigation actions are enqueued, then scope activation dispatch only occurs for the final state change, instead of potentially running into an AssertionError. (#220, thanks @valeriyo)
If you use either ScopeKey
or ScopeKey.Child
, it is advised to update, and get this bugfix.
- CHANGE: Remove dependency on
android.support.annotation.*
. With that, there should be no dependency from the library on eitherandroid.support.*
orandroidx.*
.
Replaced it using javax.annotation.Nullable
and javax.annotation.Nonnull
provided by api("com.google.code.findbugs:jsr305:3.0.2")
.
- UPDATE:
state-bundle
is updated to1.3.0
(Remove dependency onandroid.support.annotation.*
, replace withjavax.annotation.*
).
With these changes, Jetifier should no longer be needed when using Simple-Stack.
-
CHANGE:
Backstack.getSavedState(Object key).getBundle()
is now initialized to an emptyStateBundle
instead ofnull
(but is still nullable because ofsetBundle()
). -
FIX:
Backstack.persistViewToState(Object key)
no longer creates a newSavedState
instance, and usesgetSavedState
to re-use (or create) the existing one. -
FIX: Ensure that
backDispatchedServices
is also cleared after execution ofdispatchBack
.
- UPDATE:
state-bundle
is updated to1.2.2
(Fix a bug inStateBundle.equals()
).
-
FIX:
ScopedService.Activated
callback could immediately execute a navigation action, which if destroyed the current scope, then it could result in anAssertionError
(#215). -
ADD:
SimpleStateChanger
for convenience when aStateChanger
is not intended to take asynchronous execution into account.
- FIX: Ensure that unused services can be GCed inside ScopeManager (previously they could be kept alive in a map, #213).
- FIX: The explicit parent scope chain was not always resolved correctly if a key is only a ScopeKey.Child, but not a ScopeKey, and the key did not register any new scopes (as all scopes defined by the ScopeKey.Child had already been registered by previous keys).
This could provide incorrect results in findScopesForKey(key, EXPLICIT)
, and could skip a HandlesBack
service in the current top key's explicit parent chain.
- ADDED:
ScopedService.HandlesBack
.
When a service implements HandlesBack
, then when Backstack.goBack()
is called, it is first dispatched across the current active explicit scope chain.
This allows handling "back", without having to dispatch it through the active view hierarchy (in order to get access to it in scoped services).
- FIX:
Backstack.moveToTop()
did not re-order the scope hierarchy according to the new active keys (as the scope order depended on construction order, but existing scopes were not recreated).
This could have been a problem if services used the same name across multiple scopes, and the keys were re-ordered in place (not add/remove).
- ADDED:
Backstack.setGlobalServices(GlobalServices.Factory)
and itsBackstackDelegate
/Navigator
equivalent.
This allows delaying the instantiation of services when the global scope is actually being created, rather than providing them immediately.
Please note that providing a GlobalServices.Factory
will override whatever GlobalServices
was previously set.
Also note that the GlobalServices.Factory
should not be an anonymous inner class / lambda / inner class, as it is kept for configuration changes.
- DEPRECATED:
BackstackDelegate
.
With the renaming of BackstackManager
to Backstack
in 2.0.x, it's become easier to use Backstack
directly than juggling the BackstackDelegate
.
Also, using Navigator
with Fragments seems to have no side-effects, therefore this is now the preferred approach (since setting a non-default state changer calls setShouldPersistContainerChild(false)
, also since 2.0.x).
Therefore, using Navigator
is now preferred over BackstackDelegate
.
- FIX:
Backstack.forceClear()
now callsfinalizeScopes()
first to ensure that scoped services are also properly reset.
- BEHAVIOR CHANGE: The navigation operations
goBack()
,replaceTop()
,jumpToRoot()
,goUp()
,goUpChain()
, andgoTo()
(when going to existing element) are now considered "terminal" operations.
Terminal operation means that actions (that are not setHistory
) called on the Backstack are ignored while the state change of the terminal action has not been completed yet.
This is done to eliminate the possibility of enqueueing incorrect "forward" navigation immediately when a "back" navigation is happening, that could potentially create "illegal" navigation history.
Illegal navigation history is a problem when using implicit scopes, as with the right button mashing, you could potentially "skip" an expected screen, and not have registered its services.
Therefore, the possibility of this scenario is now blocked.
-
FIX: Make
findScopesForKey
work consistently even if a key is not aScopeKey
. -
FIX: Add missing
@NonNull
on Context argument on some methods of Navigator.
- FIX: Ensure that while navigation is in progress,
lookupService()
(and other service lookup methods) can access all currently built scopes, rather than only the latest navigation history currently being navigated to.
This fix is crucial when lookupService
is used inside onFinishInflate
method of views inflated by ViewPager adapters; and returning from process death, navigation to a different screen is immediate (f.ex. deep-linking via notifications).
- ADDED:
ServiceBinder.addAlias()
to allow adding an alias to an added service: available for look-up, but without getting callbacks by this registration (thus avoiding unnecessarytoBundle()
calls for a multi-registered service).
MAJOR API BREAKING CHANGES! To create better consistency and naming, certain core APIs were renamed, moved around, restructured, etc.
This means that 1.x and 2.x shouldn't be used in a project at the same time, because they're not compatible.
- BREAKING CHANGE:
BackstackManager
is nowBackstack
.
What was called Backstack
is now called NavigationCore
and is an internal class (not part of public API).
(!) This also means that the new Backstack(List<?>)
constructor is replaced with new Backstack(); backstack.setup(List<?>)
.
All sane public APIs of NavigationCore were moved over to the new Backstack (think navigation-related ones).
This means that wherever you used to receive a Backstack
, now you still receive a "Backstack", but it has additional functionality (such as lookupService
).
As a result, the following methods no longer exist:
-
BackstackDelegate.getManager()
is removed -
Navigator.getManager()
is removed -
ServiceBinder.getManager()
is removed
With that, StateChange.getBackstack()
now returns what was previously the BackstackManager
, allowing access to scope-related functions.
-
BREAKING CHANGE:
ScopedServices.Scoped
is removed, and completely replaced withScopedServices.Registered
. -
BREAKING CHANGE:
ScopedServices.Activated
behavior change, now only called once rather than multiple times if the service is in an active explicit parent chain.onScopeActive()/onScopeInactive()
->onServiceActive()
/onServiceInactive()
.
Previously, you could get multiple callbacks to onScopeActive
per each activated scope the service was in, now it is tracked per service instead and only called for 0->1
and 1->0
.
- BREAKING CHANGE:
setShouldPersistContainerChild(true)
was the default, now it is default tofalse
. It's onlytrue
by default if theDefaultStateChanger
is used.
If you use a custom-view-based setup and a custom state changer, please make sure to set Navigator.configure().setShouldPersistContainerChild(true).install(...)
.
-
BREAKING CHANGE:
ScopedServices.ServiceBinder
is now moved to top-level, now accessible asServiceBinder
. -
BREAKING CHANGE:
ViewChangeHandler.CompletionCallback
renamed toViewChangeHandler.ViewChangeCallback
. -
BREAKING CHANGE:
StateKey
is renamed toDefaultViewKey
. -
BREAKING CHANGE:
HistoryBuilder
is nowHistory.Builder
. The deprecatedHistoryBuilder
factory methods were removed. -
BREAKING CHANGE:
StateChange.getPreviousState()
->StateChange.getPreviousKeys()
StateChange.getNewState()
->StateChange.getNewKeys()
StateChange.topPreviousState()
->StateChange.topPreviousKey()
StateChange.topNewState()
->StateChange.topNewKey()
StateChange.isTopNewStateEqualToPrevious()
->StateChange.isTopNewKeyEqualToPrevious()
-
BREAKING CHANGE:
StateChange.backstack()
->StateChange.getBackstack()
. -
BREAKING CHANGE: ServiceBinder method renaming for sake of consistency.
ServiceBinder.add()
->ServiceBinder.addService()
ServiceBinder.has()
->ServiceBinder.hasService()
ServiceBinder.get()
->ServiceBinder.getService()
ServiceBinder.canFind()
->ServiceBinder.canFindService()
ServiceBinder.lookup()
->ServiceBinder.lookupService()
ServiceBinder.canFindFrom()
->ServiceBinder.canFindFromScope()
ServiceBinder.lookUpFrom()
->ServiceBinder.lookupFromScope()
-
CHANGE/FIX: Added missing
@NonNull
annotation onKeyContextWrapper.getKey()
. Now throws exception if the key is not found (previously returned null).
-
FIX:
onServiceUnregistered()
was called multiple times if the service was being unregistered from a scope where it was registered multiple times. -
SAMPLE UPDATE: Safer version of the FragmentStateChanger that handles re-entrancy of
back
and going to the same target as where we were (handlefragment.isRemoving
). -
SAMPLE UPDATE: MVP/MVVM samples have a better packaging structure.
-
ADD:
ScopedServices.Registered
to receive a service lifecycle callback when a service is added to a scope for the first time (it was not in any other scopes). -
ADD:
findScopesForKey(Object key, ScopeLookupMode mode)
to retrieve the list of scopes accessible from a given key. -
FIX: A service registered in multiple scopes would receive
fromBundle(stateBundle)
callback in each scope where it was registered and when that given scope was entered, restoring potentially outdated state on back navigation.
Now, a service will only receive fromBundle
callback before its onServiceRegistered()
callback, and not before each onEnterScope(scopeTag)
callbacks.
-
FIX: during
backstackManager.finalizeScopes()
,onExitScope
andonScopeInactive
were dispatched in an incorrect order across nested explicit parents. -
CHANGE:
persistViewToState()
/restoreViewFromState()
now use a separate bundle from the one that's publicly visible on SavedState.
-
FIX: calling
backstackManager.finalizeScopes()
multiple times now results in consistent and defined behavior (namely, it gets ignored). -
CHANGE: navigation that occurs after
backstackManager.finalizeScopes()
will now trigger reconstruction and proper callbacks of services in a consistent manner. -
FIX:
ScopedServices.Activated
'sonScopeInactive()
during scope finalization was called in order from explicit parent to child, instead of child to explicit parent.
-
FIX: NPE when
canFindFromScope()
was used on an uninitialized stack, instead of returningfalse
. -
ADD:
ScopeLookupMode
forcanFindFromScope()
andlookupFromScope()
methods, which allows restricting the lookup only to the explicit parent chain). -
ADD:
setGlobalServices()
to allow setting global services (that functions as the last parent of any parent chains).
-
FIX: Fix that a service registered multiple times in the same scope with different tags would receive service lifecycle callbacks as many times as it was registered.
-
ADD: Convenience method
stateChange.isTopNewStateEqualToPrevious()
to replacestateChange.topNewState<Any>() == stateChange.topPreviousState()
condition check. It does the same thing, but maybe it's a bit easier to read. -
FIX: Fix a typo which resulted in not throwing if the provided service tag was
null
(whoops. -_-) -
UPDATE:
Backstack
now checks if altering methods are called from the thread where the backstack was created.
-
UPDATE: Added
simple-stack-example-scoping
to showcase the usage ofScopeKey.Child
, with Fragments and Navigator. -
ADDED:
lookupFromScope()
andcanFindFromScope()
methods that begin the lookup from the specified scope, instead of the active-most one. This is to allow safer look-ups when the same service tag is used in different scopes. -
UPDATE: Better error message if the scope does not exist for lookup (also provides the currently accessed scopes).
- ADDED: Adds
ScopeKey.Child
interface, which allows the definition of explicit parent hierarchy of a given scope.
Even by default, there is an implicit hierarchy between screens: it is possible to look up services defined by previous keys.
However, there are times when we must define scopes that are supersets of multiple screens. In this case, we know we are on a given screen, within a given state, and we require a superscope to exist that is shared across multiple screens.
In this case, the key can define an explicit parent hierarchy of scopes. These scopes are created before the key's own scope (assuming the key is also a ScopeKey).
The parent scopes are only destroyed after all their children are destroyed.
lookupService()
prefers explicit parents, however will also continue to seek the service across implicit parents, and their explicit parent chain as well.
-
CHANGE: When
lookupService
cannot find the service, the exception message is improved (and tells you what could be wrong in your configuration). -
UPDATE:
mvp-view
,mvp-fragments
andmvvm-fragments
samples now useScopedServices
(andBundleable
) to retain presenters/viewmodels across config changes and have their states persisted/restored across process death.
-
CHANGE:
AnimatorViewChangeHandler
has an overridable method calledresetPreviousViewValues()
, which receives the previous view after animation is complete. -
CHANGE:
FadeViewChangeHandler
andSegueViewChangeHandler
now resetalpha = 1f
andtranslationX = 0f
respectively, after animation is complete and the view is removed.
- ADDED:
ScopedServices.Activated
. ImplementingActivated
for a scoped service makes the service receive a callback when the scope it is bound to becomes the top-most scope, and when it stops being the top-most scope.
There are strict ordering guarantees that onEnterScope
, onScopeActive
, onScopeInactive
, onExitScope
are called in this order.
onScopeInactive
is called in reverse order (just like onExitScope
).
When navigating from one scope to another scope, the new scope becomes active before the previous scope becomes inactive.
-
CHANGE:
backstack.top()
andbackstack.root()
now throw exception (just likefromTop()
) if the method is called before a StateChanger is set (and the backstack becomes initialized). This makes usingroot()
/top()
nicer in Kotlin. -
FIX: During a second "initialize" state change (which happens when calling
setStateChanger()
), accessing the Backstack'sgetHistory()
,top()
androot()
inside theStateChanger
could return incorrect value. -
UPDATE: MVP samples are now written in Kotlin. MVVM sample now has a better SQLite-based non-Room reactive wrapper (for people trying out SQLite without Room). Some samples were renamed.
- ADDED:
ServiceBinder.getBackstack()
method. This allows scoped services to be given the backstack as constructor argument.
-
ADDED:
Navigator.hasScope(scopeTag)
,BackstackDelegate.hasScope(scopeTag)
,BackstackManager.hasScope(scopeTag)
. -
ADDED:
Navigator.canFindService(Context, serviceTag)
,BackstackDelegate.canFindService(serviceTag)
,BackstackManager.canFindService(serviceTag)
to check iflookup
can find the service. -
ADDED:
ServiceBinder.lookup()
andServiceBinder.canFind()
to inherit from currently existing scopes while creating service binding. -
CHANGE:
onExitScope(scopeTag)
is now ensured to happen in reverse order compared toonEnterScope(scopeTag)
(both in terms of scope creation order and service binding order).
-
ADDED:
Navigator.isNavigatorAvailable(Activity)
to ensure the ability to check if theBackstackHost
is added to the Activity. -
ADDED:
BackstackManager.lookupService(serviceTag)
,BackstackDelegate.lookupService(serviceTag)
, andNavigator.lookupService(Context, serviceTag)
, which attempts to look up the service in all currently existing scopes (starting from the newest added scope).
-
UPDATE:
state-bundle
is updated to 1.2.1. -
CHANGE: Allow calling
BackstackDelegate.setScopedServices(activity, scopedServices)
once after anonDestroy()
callback (to allow setting back the Activity). -
CHANGE: Allow calling
BackstackDelegate.setScopedServices(null, scopedServices)
. In this case,onDestroy()
will finalize scopes - normally it only does that if Activity is finalizing.
-
API CHANGE:
backstackDelegate.setScopedServices(ScopedServices)
is nowbackstackDelegate.setScopedServices(Activity, ScopedServices)
. -
FIX: If enclosing Activity is destroyed (
onDestroy
) andActivity.isFinishing()
, then the existing scopes are destroyed along with it so thatScoped.onExitScope()
is called properly, and resources are cleaned up as expected across closing the app and restarting it quickly. -
MINOR FIX: Added
ScopedServices
javadoc and missing@NonNull
on ServiceBinder. -
DEPRECATED:
reset()
method. Renamed toforceClear()
. You probably don't need it, but that wasn't clear enough.
- ADDED: Ability to share data across screens via scoped services.
To use, the key must implement ScopeKey
and define its scope's tag. If ScopeKey
is used, then a ScopedServices
must be provided.
Currently, a scope can be shared across keys, but there is no scope inheritance, and there is no way to have multiple scopes for a given key.
To use, one must set an implementation of ScopedServices
on either BackstackDelegate
, Navigator.Installer
, or BackstackManager
.
Services that are Bundleable
and registered in a given scope receive callbacks to toBundle()
and fromBundle()
to persist/restore their state.
Services registered in a given scope receive onEnterScope()
and onExitScope()
callbacks if they implement ScopedServices.Scoped
.
Tip: A possible way to simplify the usage of service tags
is to define an inline reified extension function for ServiceBinder
to default to using T::class.java.name
.
-
ADDED: Ability to change duration, interpolation and start delay of
AnimatorViewChangeHandler
. -
ADDED:
jumpToRoot(direction)
.
- MINOR CHANGE:
DefaultStateChanger
usesFadeViewChanger
forStateChange.REPLACE
by default, instead ofNoOpViewChangeHandler
.
-
Fix:
History.single()
should returnHistory<T>
, notList<T>
. -
ADDED:
jumpToRoot()
andmoveToTop()
convenience operators toBackstack
. -
ADDED:
goUp(Object, boolean fallbackToBack)
andgoUpChain(List<?>, boolean fallbackToBack)
to allow opt-in for the newly provided navigation principle: "Up and Back are equivalent within your app's task"
- DEPRECATED: HistoryBuilder's factory methods are moved from HistoryBuilder to the newly added
History
class.
HistoryBuilder.from(T... objects)
-> History.builderOf(T... objects)
HistoryBuilder.from(...)
-> History.builderFrom(...)
HistoryBuilder.single()
-> History.single()
HistoryBuilder.newBuilder()
-> History.newBuilder()
Also adds History.of(T... objects)
instead of HistoryBuilder.of(...).build()
.
- ADDED:
History
class, an immutable list with additional operations overList<T>
- as some methods' return type.
I also changed some return types (in history building) from List<Object>
to <T> List<T>
,
this resulted in having to change some List<Object>
s to List<?>
on the receiving side if used in assignment.
So if you run into that, just change List<Object>
to List<?>
and it should work.
-
ADDED: Long-overdue empty constructor for
BackstackDelegate
andbackstack.setStateChanger(StateChanger)
. -
UPDATE: Kotlin sample replaces
PaperParcel
with@Parcelize
.
-
CRITICAL FIX:
1.8.1 (2018-01-17)
didn't retrievestate-bundle 1.2.0
as transitive dependency because ofcom.github.dcendents:android-maven-gradle-plugin:1.5
. It is updated to2.0
to fix publishing to Jitpack. -
ADDED / CHANGE:
Navigator.findActivity(Context)
is now public. It also casts the returned Activity to whatever subclass type is expected. -
ADDED:
Backstack.fromTop(int offset)
method, which provides the element in the backstack from the top with a given offset. -
Updated State-Bundle to 1.2.0.
-
Updated to use implementation/api and AS 3.0's tooling.
-
Updated implementation lib versions in samples and tests.
-
Updated Kotlin example to use Fragment-based usage.
- BREAKING(?) CHANGE / FIX: when
goBack()
returns false, then the backstack is not cleared automatically. Addedreset()
to allow imitating the previous behavior.
Previous behavior would now be the same as:
if(!backstack.goBack() && !backstack.isStateChangePending()) {
backstack.reset();
}
Honestly, this might have been unexpected, as goBack()
returning false
had the side-effect of clearing the stack, and next state change using the initial key!
The test that checks for this has been changed to use the above construct. Another test of course has been added to validate new behavior.
Also, to eliminate the possibility of reset()
misuse, it is only allowed when there are no pending state changes.
-
BREAKING CHANGE:
getInitialParameters()
is renamed togetInitialKeys()
. -
FIX:
getInitialParameters()
returned the actual list instead of an unmodifiable copy, it returns the keys provided at initialization. -
ADDED:
replaceTop()
backstack operator, which replaces current top with the provided key. -
ADDED:
goUp()
backstack operator, which will go back to the provided key if exists, replace top with new key otherwise. -
ADDED:
goUpChain()
backstack operator, which will:- If the chain of parents is found as previous elements, then it works as back navigation to that chain.
- If the whole chain is not found, but at least one element of it is found, then the history is kept up to that point, then the chain is added, any duplicate element in the chain is added to the end as part of the chain.
- If none of the chain is found, the current top is removed, and the provided parent chain is added.
I added a bunch of tests for this, hopefully I didn't forget anything!
-
ENHANCEMENT/FIX:
HistoryBuilder.get()
is now@NonNull
, becauseHistoryBuilder.from(List<?>)
throws if List containsnull
. -
ENHANCEMENT:
getHistory()
andgetInitialParameters()
also returns aList<T>
in which each element is cast toT
. -
FIX:
BackstackDelegate.setStateChanger()
should have been allowed even without callingbackstackDelegate.onCreate()
first. All the samples usenew BackstackDelegate(null)
so it never came up. -
ENHANCEMENT: Some improvement to
persistViewToState()
exception message if the view has no key (so that it referencesKeyContextWrapper
andstateChange.createContext()
).
- MINOR CHANGE + ENHANCEMENT:
StateChange.getNewState()
andStateChange.getPreviousState()
return a copy of the list (it was already a copy, don't worry), where each item is casted to<T>
specified as generic parameter.
For example, the following can be changed from:
for(Object _newKey : stateChange.getNewState()) {
Key newKey = (Key)_newKey;
// ...
}
to:
for(Key newKey : stateChange.<Key>getNewState()) {
// ...
}
And the following works now as well:
List<Key> newKeys = stateChange.getNewState(); // used to return List<Object>
- ADDED:
BackstackDelegate.registerForLifecycleCallbacks(Activity)
convenience method (API 14+).
This method allows you to call this after BackstackDelegate.onCreate()
, after which the following 4 methods no longer need to be called manually:
- `onPostResume()`
- `onPause()`
- `onSaveInstanceState(Bundle)`
- `onDestroy()`
Therefore the callbacks that ought to be called remain as onCreate()
, onRetainCustomNonConfigurationInstance()
, and of course onBackPressed()
.
- REMOVED:
BackstackManager.StateChangeCompletionListener
. It is replaced byBackstack.CompletionListener
, which was added back in 0.9.1 (and is more reliable).
This also fixes a possible bug with incorrect call order of state change completion listeners.
The API is otherwise exactly the same, StateChangeCompletionListener
should have been Backstack.CompletionListener
from the start.
- ADDED:
backstackDelegate.getManager()
, just to make sure its API mirrorsNavigator
.
- Added missing
@NonNull
and@Nullable
annotations where applicable.
-
MINOR CHANGE:
DefaultStateChanger
no longer explicitly demands aStateKey
, because bothLayoutInflationStrategy
andGetViewChangeHandlerStrategy
can be re-defined for custom behavior. -
Added
GetViewChangeHandlerStrategy
toDefaultStateChanger
to allow re-defining the view change handler behavior. -
Added
ContextCreationStrategy
toDefaultStateChanger
to support Mortar scopes, or anything similar in design. -
Added
BackstackManager.StateChangeCompletionListener
to add a hook where you can listen for the completion of state changes reliably - even if they were forced to execute.
Also added addStateChangeCompletionListener
to BackstackDelegate
and Navigator.Installer
accordingly.
Please make sure it does not retain a reference to enclosing Activity, to avoid memory leaks.
-
Minor fix:
setKeyFilter()
,setKeyParceler()
, andsetStateClearStrategy()
inBackstackDelegate
now throw if they are set after callingonCreate()
, as per docs. -
Bump
state-bundle
version to1.1.5
-
ADDED:
simple-stack-mortar-sample
, which is based onmortar-sample
fromsquare/mortar
, but usingSimple-Stack
andService-Tree
.
- Added
GetPreviousViewStrategy
toDefaultStateChanger
as per request (#36).
- Added
KeyFilter
to allow clearing out keys on process death that should not be restored. - Added
stateChange.backstack()
which returns the backstack this state change was executed by. - Added
DefaultStateChanger.ViewChangeStartListener
for before the view change, but after the state restore - Added
DefaultStateChanger.LayoutInflationStrategy
to support asynchronous layout inflation (if you need it) or hopefully Anko
- Bump
state-bundle
version to1.1.4
- Bump
state-bundle
version to1.1.0
- Added a method to
DefaultStateChanger
to allow perform view change with an externally specified direction.
-
Merged
zhuinden/navigator
intozhuinden/simple-stack
. -
ADDED:
Navigator
class as an optional replacement forBackstackDelegate
(API 11+).
BackstackDelegate
had a lot of callbacks to remember (onPause()
, onResume()
, onRetainCustomNonConfigurationInstance()
, onDestroy()
),
but more importantly you had to manage saving out the current view's state in onSaveInstanceState()
manually.
With Navigator, this is all hidden in the BackstackHost
installed by Navigator.install()
(or Navigator.configure()...install()
, so this problem is solved for you.
- ADDED:
DefaultStateChanger
that by default usesNavigator
-based installation.
To use DefaultStateChanger
with BackstackDelegate
, you must provide DefaultStateChanger.configure().setStatePersistenceStrategy()
and delegate persistence calls to your delegate.
-
ADDED:
StateKey
interface used byDefaultStateChanger
. -
ADDED: default view change handlers for
DefaultStateChanger
. -
All examples (except the
fragment
andmultistack
samples) were updated to useNavigator
. -
simple-stack-example-mvp
no longer usessquare/coordinator
, it uses custom viewgroups instead.
This is because Coordinator
gets created only after container.addView()
, which makes it hard to work with.
- ADDED:
backstack.top()
method that returns the last element in the backstack ornull
- FIX: Fixed a bug that if a restored backstack is cleared and an initialize state change is triggered, then the restored keys were used instead of the initial key (this only surfaced if you attempt to use multiple backstacks, and a cleared backstack is re-used)
- CHANGE: Decreased minSDK to 1.
- MINOR FIX: Adjusted exception message in
BackstackManager
to saysetup()
.
-
BREAKING CHANGE:
StateBundle
is moved fromcom.zhuinden.simplestack.StateBundle
tocom.zhuinden.statebundle.StateBundle
-
CHANGE:
StateBundle
is moved to https://github.com/Zhuinden/state-bundle and is a compile dependency ofsimple-stack
-
REFACTOR:
BackstackDelegate
is separated intoBackstackManager
. -
ADDED:
BackstackManager
class to help with creating backstacks inside views. -
ENHANCEMENT:
BackstackManager
is nowBundleable
, therefore its state can be automatically restored along with other managed services (see examples). -
CHANGE:
clearStatesNotIn()
is no longer a method ofBackstackDelegate
orBackstackManager
, it can be specified as custom using aBackstackManager.StateClearStrategy
. -
ADDED:
simple-stack-example-services
that shows how to useservice-tree
withsimple-stack
to store scoped services that can have their states restored and survive configuration change. -
ADDED:
simple-stack-example-nestedstack
that shows how to use view-levelBackstackManager
stored inservice-tree
to have nested backstacks inside views.
- FIX:
HistoryBuilder
should receiveList<?>
as parameters, notList<Object>
on all methods.
-
BREAKING CHANGE:
Bundleable
andSavedState
now useStateBundle
class. -
ENHANCEMENT: Added
StateBundle
class to replaceandroid.os.Bundle
.
-
FIX: A bug that allowed the possibility that an uninitialized backstack could restore its history to be an empty list after process death.
-
ADDED:
simple-stack-example-multistack
for handling multiple backstacks in the same Activity using a BottomNavigationView.
-
BREAKING CHANGE:
Backstack
's APIs returnObject
instead ofParcelable
(that includesStateChange
, initial keys,HistoryBuilder
, etc.) -
ENHANCEMENT: Added
KeyParceler
interface to allow defining custom strategy in order to turn keys intoParcelable
(for example, usingParceler
library instead)
-
RELEASE: 1.0.0!
-
ENHANCEMENT: Javadoc for all public classes and methods.
- INTERNAL CHANGE: Hidden
stateChangeComplete()
from public API ofBackstackDelegate
, it shouldn't have been public.
-
INTERNAL CHANGE:
clearStatesNotIn()
now receives bothkeyStateMap
andStateChange
, instead of just the new state. -
ENHANCEMENT: Added
HistoryBuilder.from(Backstack)
andHistoryBuilder.from(BackstackDelegate)
convenience methods. -
ENHANCEMENT: Added
HistoryBuilder.isEmpty()
method, and implementsIterable<Parcelable>
. -
ADDED:
flow-masterdetail-fragments
example. -
FIX: A bug in
flow-masterdetail
sample that prevented Master's state from being persisted if detail directly opens a detail.
-
ENHANCEMENT: Added ability to force execute pending state changes with
Backstack.executePendingStateChange()
. -
INTERNAL CHANGE:
BackstackDelegate.onDestroy()
callsbackstack.executePendingStateChange()
to prevent hanging state changes. -
ADDED:
ObjectAnimator
-based segue animation to MVP example.
-
BREAKING CHANGE(?):
CompletionListener
no longer receivesisPending
parameter. -
ADDED:
Backstack.isStateChangePending()
to replaceisPending
. -
ENHANCEMENT: Added some missing
@NonNull
and@Nullable
annotations. -
ADDED: Apache license notes, and improved the README.
-
BREAKING CHANGE(!):
BackstackDelegate
has a new method which must be called:backstackDelegate.onDestroy()
Not callingbackstackDelegate.onDestroy()
will most likely result in memory leak, so please make sure you call it paired withonCreate()
. -
BREAKING CHANGE:
BackstackDelegate.clearStatesNotIn()
is no longer public, because it is automatically managed on state change completion. -
ENHANCEMENT: Added
Backstack.CompletionListener
which listens to when backstack has completed a state change. AddedBackstack.addCompletionListener()
andBackstack.removeCompletionListener()
methods. The backstack keeps a strong reference to your completion listener, so make sure you remove your change listener when no longer needed. -
ENHANCEMENT: It is no longer the responsibility of the
StateChanger
to callbackstackDelegate.clearStatesNotIn()
. TheBackstackDelegate
registers itself as aCompletionListener
, and therefore it can callclearStatesNotIn()
automatically. -
ENHANCEMENT: Added
flow-sample
changed to use Simple-Stack, as namesimple-stack-flow-masterdetail
.
- ENHANCEMENT: Added
BackstackDelegate.setPersistenceTag(String)
for support of multiple backstacks. It must be called beforeBackstackDelegate.onCreate()
.
- CHANGE:
KeyContextWrapper
is public again - ENHANCEMENT: Created
fragments
example based onmvp
example.
- BREAKING(?) CHANGE: Renamed
HistoryBuilder.peek()
toHistoryBuilder.getLast()
- ENHANCEMENT: Added the following new methods to
HistoryBuilder
:HistoryBuilder.get(index)
,HistoryBuilder.contains(key)
,HistoryBuilder.containsAll(keys)
,HistoryBuilder.add(key, index)
,HistoryBuilder.size()
,HistoryBuilder.removeAt(index)
,HistoryBuilder.remove(key)
,HistoryBuilder.clear()
,HistoryBuilder.retainAll(keys)
,HistoryBuilder.indexOf(key)
,
- BREAKING CHANGE: Removed
StateChange.Direction
, it is now anint
annotated with@IntDef
. This means thatStateChange.Direction.FORWARD
is nowStateChange.FORWARD
, same forBACKWARD
andREPLACE
. - Fix:
@StateChangerRegisterMode
shouldn't have been public
- BREAKING CHANGE: Removed
Backstack.get(Context)
,BackstackDelegate.isSystemService(String)
andBackstackDelegate.getSystemService(Context)
.
These can be easily done manually with the following setup:
public static Backstack get(Context context) {
// noinspection ResourceType
return (Backstack)context.getSystemService(BACKSTACK);
}
and
@Override
public Object getSystemService(String name) {
if(name.equals(BACKSTACK)) {
return backstackDelegate.getBackstack();
}
return super.getSystemService(name);
}
Therefore the preferred solution is to provide the Backstack
instance via @Inject
instead of Backstack.get(Context)
.
Example for Backstack.get(Context)
was moved to simple-stack-example
as BackstackService
.
Example for @Inject Backstack backstack;
is seen in simple-stack-example-mvp
.
- It is now allowed to initialize
BackstackDelegate
without aStateChanger
, in which casesetStateChanger()
must be called beforeonPostResume()
.This way it is possible to postpone the initialization state change of the
Backstack`.
- Simple Stack is now a library!
- Added
BackstackDelegate.getBackstack()
for convenience overBackstack.get(this)
in Activity
- Added
Bundleable
interface to allow saving view's state to Bundle - Added
BackstackDelegate.restoreViewFromState()
method to mirrorpersistViewToState()
getSavedState()
now returns a newSavedState
instead of throwing error if the key has no state bound to it- Added
SavedState.viewHierarchyState
default valuenew SparseArray<>()
, null is prohibited
- Added
BackstackDelegate
class to hide activity lifecycle integration - Moved
SavedState
into library - Added
Backstack.get(Context)
method to obtain Backstack of instance shared by the delegate - Moved
KeyContextWrapper
into library, and it is now package-private - Added
StateChange.createContext(base, key)
method to createKeyContextWrapper
KeyContextWrapper.getKey(Context)
is nowBackstack.getKey(Context)
- Rename packages from
demostack
tosimplestack
- Rename
State
toSavedState
- Add check for if
key
isnull
inState
'sBuilder.build()
- Added missing
equals()
/hashCode()
toState
class in example
- Added view persistence to example code (
MainActivity
)
- Added
HistoryBuilder
for convenience
- Minor bug fixes and simplifications
- Added
ReentranceTest
and ported tosimple-stack-demo
codebase - Fixed some bugs based on
ReentranceTest
- all tests pass now
- State changes are now enqueued while
StateChanger
is not available (afteronPause()
) or a state change is already in progress - Added
FlowTest
and ported tosimple-stack-demo
codebase
- Key and backstack are now provided to custom viewgroup via
getSystemService()
- Added initial
Backstack
,StateChange
andStateChanger
classes. - Backstack allows manipulation of state via
goTo()
,goBack()
andsetHistory()
. - Demo persists backstack history through config change and process death.
Limitations:
- ViewState is not persisted
- scheduling state changes (starting a state change while another is in progress) is not allowed
- there is a possibility that state change can occur even after
onPause()
- key and backstack are manually set to the custom viewgroup, which means these are not directly accessible in their child views (and the interfaces are ugly anyways)