Migration von WillPopScope zu PopScope
Deprecation ist etwas natürliches in der Softwareentwicklung. Code Teile werden als veraltet markiert, Alternativen werden geschaffen und die Entwickler wechseln auf die neuen Klassen und Methoden. Leider kommt es immer wieder vor, dass die angebotene Alternative anders funktioniert als erwartet oder gewünscht und es entsprechend zu Problemen kommt.
Selbiges Problem habe ich mit der Migration von WillPopScope open_in_new zu PopScope open_in_new. Beide sind dafür zuständig in einer Flutter App die normale Behandlung des Back-Buttons zu verändern. Ich nutze dies z.B. in meiner Tessa App. Mit WillPopScope gab es keine Probleme, aber aufgrund einer Änderung im Android System mussten die Flutter Entwickler hier etwas neues schaffen, um auch die Funktionen in neueren Android Version zu unterstützen.
Während WillPopScope für meine verschiedenen Fälle immer funktionierte, nutzt PopScope einen anderen Ansatz und zwang mich zum umdenken. Dabei geht es nicht nur mir so, wie der GitHub Issue open_in_new zum Thema zeigt.
Konkret war einer meiner Usecases das Back-Handling auf dem Hauptbildschirm. Folgende Actions sollen dabei in absteigender Reihenfolge geprüft und falls nötig ausgeführt werden, dabei wird immer nur eine Aktion ausgeführt.
- Ist der Navigation Drawer offen, schließe ihn
- Befindet sich der Nutzer nicht im ersten Tab, navigiere dort hin zurück
- Wird die App schließen Warnung nicht angezeigt, zeige die Warnung an
- Wird die App schließen Warnung angezeigt, beende die App
Dieser Flow eliminiert das normale Back-Handling komplett. Mit WillPopScope konnte ich dies einfach umsetzen, denn wie der Name suggeriert wird hier vor der eigentlichen Back Aktion eingegriffen und man kann selbige unterbrechen und Aktionen vorher ausführen. Mit PopScope kann man nun generell entscheiden ob die Back-Action stattfinden soll und hat dann eine Methode die weitere Aktionen erlaubt, welche aber nachdem die Back-Action bereits stattgefunden hat ausgeführt wird. Dies ist für verschiedene Actions zu spät, woraus sich für mich ergab die Back-Action generell zu unterbrechen und das gesamte Handling selbst zu machen. Dies funktioniert vor allem in Fällen wie dem oben erwähnten, wo ohnehin gar kein normales Back-Handling stattfindet. Sofern es aber einen Fall gibt indem man ganz normal zurück navigieren will ist Navigator.pop(context);
da um euch zu helfen.
Für mich ergab sich daraus folgende Migration, wobei das InterceptBack
Widget (siehe unten) einfach nur ein Wrapper ist, um Wiederholungen von Code zu vermeiden.
Alter Code
WillPopScope(
onWillPop: () async {
if (_isDrawerOpen()) { // Navigation Drawer offen, schließe ihn
return true;
} else if (_page != NavigationPage.calendar) { // Erster Tab nicht aktiv, navigiere dort hin
setState(() => _page = NavigationPage.calendar);
return false;
} else {
if (_closeOnBack) { // App schließen Warnung sichtbar, beende App
return true;
} else { // App schließen Warnung nicht sichtbar, zeige Warnung
_closeOnBack = true;
showSnackBar(context, AppLocalizations.of(context)!.rootBackAgain, duration: kSnackBarDefault);
Timer(const Duration(seconds: kSnackBarDefault), () => _closeOnBack = false);
return false;
}
}
},
child: child);
Neuer Code
InterceptBack(
onIntercept: () {
if (_isDrawerOpen()) { // Navigation Drawer offen, schließe ihn
_scaffoldKey.currentState?.closeDrawer();
} else if (_page != NavigationPage.calendar) { // Erster Tab nicht aktiv, navigiere dort hin
setState(() => _page = NavigationPage.calendar);
} else if (_closeOnBack) { // App schließen Warnung sichtbar, beende App
SystemNavigator.pop();
} else { // App schließen Warnung nicht sichtbar, zeige Warnung
_closeOnBack = true;
showSnackBar(context, AppLocalizations.of(context)!.rootBackAgain, duration: kSnackBarDefault);
Timer(const Duration(seconds: kSnackBarDefault), () => _closeOnBack = false);
}
},
child: child);
InterceptBack Wrapper
import 'package:flutter/material.dart';
class InterceptBack extends StatelessWidget {
final Widget child;
final Function onIntercept;
const InterceptBack({required this.child, required this.onIntercept, super.key});
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false, // Normales Back-Handling deaktivieren
onPopInvoked: (bool didPop) {
if (didPop) {
return;
}
onIntercept();
},
child: child);
}
}