Closed manueldidonna closed 4 years ago
An Activity
's onBackPressed()
normally finishes the Activity.
The AcornActivity
overrides Activity.onBackPressed()
to prevent the Activity from closing and allowing the Navigator
to handle back press events, using the OnBackPressListener
.
If the Navigator returns true
in its onBackPressed()
function, it is assumed the Navigator successfully handled the event and no further action is necessary. If it returns false
, it is assumed the Navigator could not handle the back press, and the Activity's own onBackPressed()
function is called, finishing the Activity: https://github.com/nhaarman/acorn/blob/dc0e051402cf24eefb369d3efb480cfa5e95ef56/ext/acorn-android/src/main/java/com/nhaarman/acorn/android/AcornActivity.kt#L178-L183
Thus, in short, returning true
from the root Navigator's onBackPressed()
will not finish the Activity, returning false
will finish it.
Giving it a thought, https://github.com/nhaarman/acorn/pull/144 introduced routing the back press to the currently active ViewController. If your ViewController implements the OnBackPressedListener interface, you have the control to intercept the event there.
I would probably handle such a requirement here, since you might want to show a notification to the user on the first back press. This will result in a similar setup as if you were to add a button to exit the app:
interface MyContainer {
fun setOnBackPressListener(f: () -> Unit)
}
class MyViewController(...) : ViewController, MyContainer, OnBackPressedListener {
private var onBackPressedListener: (() -> Unit)? = null
override fun setOnBackPressListener(f: () -> Unit) {
onBackPressedListener = f
}
private var backPressCount = 0
override fun onBackPressed() : Boolean {
backPressCount++
if(backPressCount < 2) {
// Return true here to 'claim' the back press event and prevent the Activity from being finished
return true
}
onBackPressedListener?.invoke()
// Always return true here, we're manually handling the back event now.
return true
}
}
class MyScene(
private val listener: Events
): Scene<MyContainer> {
override fun attach(c: MyContainer) {
c.setOnBackPressListener { listener.onBackPressed() }
}
interface Events {
fun onBackPressed()
}
}
Then call finish()
in the Navigator when you implement the MyScene.Events
interface. Something like this should work, I've typed this out without the compiler 🙃
Thus, in short, returning true from the root Navigator's onBackPressed() will not finish the Activity, returning false will finish it.
You can see that my simplified version of fun onBackPressed(): Boolean
from a CompositeStackNavigator
always return true but the activity will be normally finished
That is because you're invoking super.onBackPressed()
, which will result in an empty stack in the CompositeStackNavigator
, finishing the Activity because of that (Navigator.Events.finished()
will be invoked).
I got it, is there a way to avoid it happening without rewriting the entire navigator implementation?
I've edited my comment above with a sample, does that help?
An easier sample without the manual routing through the Scene (which might work well enough as well):
interface MyContainer : Container
class MyViewController(...) : ViewController, MyContainer, OnBackPressedListener {
private var backPressCount = 0
override fun onBackPressed() : Boolean {
backPressCount++
if(backPressCount < 2) {
// Return true here to 'claim' the back press event and prevent the Activity from being finished
return true
}
// Return false here, allowing the Navigator to handle the back press.
return false
}
}
An easier sample without the manual routing through the Scene (which might work well enough as well)
The scene could be present many times in the stack, and the back twice behavior should happen only when that scene is the last one.
Anyway I really apprecciate your work, using Acorn is a pleasure (far better than fragments). I love how you have separate the responsabilities of ui, data managment and navigation into several components but I think that Navigators are the least flexible components. This kind of issue proves it. I have to rewrite their implementation for every minimal behavior change
Here is my solution. I hope this could be helpful
class Navigator : Navigator.Events {
// true by default
var isRootSceneActive = true
var onLastBackPressListener: (() -> Boolean)? = null
// this listener is added to every child navigator
override fun scene(scene: Scene<out Container>, data: TransitionData?) {
isRootSceneActive = **some black magic based on the current scene**
}
override fun onBackPressed(): Boolean {
if (!isRootSceneActive || onLastBackPressListener?.invoke() == false)
return super.onBackPressed()
else
return true
}
}
class Activity {
override fun onCreate(savedInstanceState: Bundle?) {
navigator().onLastBackPressListener = { **back press twice logic** }
}
override fun onDestroy() {
// don't leak activity
navigator().onLastBackPressListener = null
}
}
I'm having some trouble to understand how the back press is handled by navigators
I also tried to register an OnBackPressedCallback on backPressDispatcher but it doesn't work.
How could I achieve a back twice to close behavior?