Skip to content

Commit 26b35f3

Browse files
authored
#259 Add support for Backstack.setBackHandlingModel(AHEAD_OF_TIME) (#271)
* #259 Add support for Backstack.setBackHandlingModel(AHEAD_OF_TIME) * #259 Update (most) samples to use android:enableOnBackInvokedCallback * #259 Update some dependencies in samples * #259 Update readme and changelog * #259 Remove accidental "res/res" folder * #259 formatting
1 parent c2d55fe commit 26b35f3

109 files changed

Lines changed: 2930 additions & 833 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,211 @@
11
# Change log
22

3+
-Simple Stack 2.7.0 (2023-03-31)
4+
--------------------------------
5+
6+
- ***MAJOR FEATURE ADDITION***: Added `Backstack.setBackHandlingModel(BackHandlingModel.AHEAD_OF_TIME)` to
7+
support `android:enableBackInvokedCallback="true"` on Android 14 for predictive back gesture support.
8+
9+
With this, `Navigator.Installer.setBackHandlingModel()`, `BackstackDelegate.setBackHandlingModel()`,
10+
and `Backstack.setBackHandlingModel()` are added.
11+
12+
Also, `ServiceBinder.getAheadOfTimeBackCallbackRegistry()` is added as a replacement for `ScopedServices.HandlesBack`.
13+
Please note that using it requires `AHEAD_OF_TIME` mode, and without it, trying to
14+
use `ServiceBinder.getAheadOfTimeBackCallbackRegistry()` throws an exception.
15+
16+
Also, `Backstack.willHandleAheadOfTimeBack()`, `Backstack.addAheadOfTimeWillHandleBackChangedListener()`
17+
and `Backstack.removeAheadOfTimeWillHandleBackChangedListener()` are added.
18+
19+
**IMPORTANT**:
20+
21+
The `AHEAD_OF_TIME` back handling model must be enabled similarly to how `setScopedServices()` or other similar configs
22+
must be called before `backstack.setup()`, `Navigator.install()`, or `BackstackDelegate.onCreate()`.
23+
24+
When `AHEAD_OF_TIME` is set, the behavior of `goBack()` changes. Calling `goBack()` when `willHandleAheadOfTimeBack()`
25+
returns false throws an exception.
26+
27+
When `AHEAD_OF_TIME` is set, `ScopedServices.HandlesBack` will **no longer be called** (as it cannot return whether a
28+
service WILL handle back or not), and should be replaced with registrations to the `AheadOfTimeBackCallbackRegistry`.
29+
30+
When `AHEAD_OF_TIME` is NOT set (and therefore the default, `EVENT_BUBBLING` is set),
31+
calling `willHandleAheadOfTimeBack` or `addAheadOfTimeWillHandleBackChangedListener`
32+
or `removeAheadOfTimeWillHandleBackChangedListener` throws an exception.
33+
34+
To migrate to use the ahead-of-time back handling model, then you might have the previous
35+
somewhat `onBackPressedDispatcher`-compatible (but not predictive-back-gesture compatible) code:
36+
37+
``` kotlin
38+
class MainActivity : AppCompatActivity(), SimpleStateChanger.NavigationHandler {
39+
private lateinit var fragmentStateChanger: DefaultFragmentStateChanger
40+
41+
@Suppress("DEPRECATION")
42+
private val backPressedCallback = object: OnBackPressedCallback(true) {
43+
override fun handleOnBackPressed() {
44+
if (!Navigator.onBackPressed(this@MainActivity)) {
45+
this.remove()
46+
onBackPressed() // this is the reliable way to handle back for now
47+
this@MainActivity.onBackPressedDispatcher.addCallback(this)
48+
}
49+
}
50+
}
51+
52+
override fun onCreate(savedInstanceState: Bundle?) {
53+
super.onCreate(savedInstanceState)
54+
55+
onBackPressedDispatcher.addCallback(backPressedCallback) // this is the reliable way to handle back for now
56+
57+
val binding = ActivityMainBinding.inflate(layoutInflater)
58+
setContentView(binding.root)
59+
60+
fragmentStateChanger = DefaultFragmentStateChanger(supportFragmentManager, R.id.container)
61+
62+
Navigator.configure()
63+
.setStateChanger(SimpleStateChanger(this))
64+
.install(this, binding.container, History.single(HomeKey))
65+
}
66+
67+
override fun onNavigationEvent(stateChange: StateChange) {
68+
fragmentStateChanger.handleStateChange(stateChange)
69+
}
70+
}
71+
```
72+
73+
This code changes to the following in order to support predictive back gesture using ahead-of-time model:
74+
75+
```kotlin
76+
class MainActivity : AppCompatActivity(), SimpleStateChanger.NavigationHandler {
77+
private lateinit var fragmentStateChanger: FragmentStateChanger
78+
79+
private lateinit var authenticationManager: AuthenticationManager
80+
81+
private lateinit var backstack: Backstack
82+
83+
private val backPressedCallback = object : OnBackPressedCallback(false) { // <-- !
84+
override fun handleOnBackPressed() {
85+
backstack.goBack()
86+
}
87+
}
88+
89+
private val updateBackPressedCallback = AheadOfTimeWillHandleBackChangedListener { // <-- !
90+
backPressedCallback.isEnabled = it // <-- !
91+
}
92+
93+
override fun onCreate(savedInstanceState: Bundle?) {
94+
super.onCreate(savedInstanceState)
95+
96+
setContentView(R.layout.main_activity)
97+
98+
onBackPressedDispatcher.addCallback(backPressedCallback) // <-- !
99+
100+
fragmentStateChanger = FragmentStateChanger(supportFragmentManager, R.id.container)
101+
102+
backstack = Navigator.configure()
103+
.setBackHandlingModel(BackHandlingModel.AHEAD_OF_TIME) // <-- !
104+
.setStateChanger(SimpleStateChanger(this))
105+
.install(this, binding.container, History.single(HomeKey))
106+
107+
backPressedCallback.isEnabled = backstack.willHandleAheadOfTimeBack() // <-- !
108+
backstack.addAheadOfTimeWillHandleBackChangedListener(updateBackPressedCallback) // <-- !
109+
}
110+
111+
override fun onDestroy() {
112+
backstack.removeAheadOfTimeWillHandleBackChangedListener(updateBackPressedCallback); // <-- !
113+
super.onDestroy()
114+
}
115+
116+
override fun onNavigationEvent(stateChange: StateChange) {
117+
fragmentStateChanger.handleStateChange(stateChange)
118+
}
119+
}
120+
```
121+
122+
Please make sure to remove the `AheadOfTimeWillHandleBackChangedListener` in `onDestroy` (Activity) or `onDestroyView` (
123+
Fragment), because the listener staying registered would be a memory leak.
124+
125+
A "lifecycle-aware" callback *might* be added to `simple-stack-extensions` later.
126+
127+
If you can't update to the `AHEAD_OF_TIME` back handling model, then don't worry, as backwards compatibility has been
128+
preserved with the previous behavior.
129+
130+
When using `AHEAD_OF_TIME` back handling model, `ScopedServices.HandlesBack` is no longer called. To replace this, you
131+
might have had something like this:
132+
133+
```kotlin
134+
class FragmentStackHost(
135+
initialKey: Any
136+
) : Bundleable, ScopedServices.HandlesBack {
137+
var isActiveForBack: Boolean = false
138+
139+
// ...
140+
141+
override fun onBackEvent(): Boolean {
142+
if (isActiveForBack) {
143+
return backstack.goBack()
144+
} else {
145+
return false
146+
}
147+
}
148+
}
149+
```
150+
151+
This is replaced like so:
152+
153+
```kotlin
154+
class FragmentStackHost(
155+
initialKey: Any,
156+
private val aheadOfTimeBackCallbackRegistry: AheadOfTimeBackCallbackRegistry,
157+
) : Bundleable, ScopedServices.Registered {
158+
var isActiveForBack: Boolean = false
159+
set(value) {
160+
field = value
161+
backCallback.isEnabled = value && backstackWillHandleBack
162+
}
163+
164+
private var backstackWillHandleBack = false
165+
set(value) {
166+
field = value
167+
backCallback.isEnabled = isActiveForBack && value
168+
}
169+
170+
private val backCallback = object : AheadOfTimeBackCallback(false) {
171+
override fun onBackReceived() {
172+
backstack.goBack()
173+
}
174+
}
175+
176+
private val willHandleBackChangedListener = AheadOfTimeWillHandleBackChangedListener {
177+
backstackWillHandleBack = it
178+
}
179+
180+
init {
181+
// ...
182+
backstackWillHandleBack = backstack.willHandleAheadOfTimeBack()
183+
backstack.addAheadOfTimeWillHandleBackChangedListener(willHandleBackChangedListener)
184+
}
185+
186+
override fun onServiceRegistered() {
187+
aheadOfTimeBackCallbackRegistry.registerAheadOfTimeBackCallback(backCallback)
188+
}
189+
190+
override fun onServiceUnregistered() {
191+
aheadOfTimeBackCallbackRegistry.unregisterAheadOfTimeCallback(backCallback)
192+
}
193+
}
194+
```
195+
196+
Where `FragmentStackHost` gets the `AheadOfTimeBackCallbackRegistry`
197+
from `serviceBinder.getAheadOfTimeBackCallbackRegistry()`.
198+
199+
So in this snippet, whether back will be handled needs to be propagated up, and manage the enabled state of
200+
the `AheadOfTimeBackCallback` to intercept back if needed.
201+
202+
While this might seem a bit tricky, this is how Google does it in their own micromanagement of communicating with
203+
the `onBackPressedDispatcher` as well, so evaluating ahead of time who will want to handle back later is unavoidable.
204+
205+
- DEPRECATED: `BackstackDelegate.onBackPressed()` and `Navigator.onBackPressed()`. Not only are they the same
206+
as `backstack.goBack()` and merely managed to confuse people historically, but this deprecation mirros the deprecation
207+
of `onBackPressed` in compileSdk 33, to push towards using predictive back.
208+
3209
-Simple Stack 2.6.5 (2022-11-11)
4210
--------------------------------
5211

0 commit comments

Comments
 (0)