Boehrsi.de - IT und Gaming Blog

Migration von WillPopScope zu PopScope

Erstellt am event Uhr von account_circle Boehrsi in label Development
Migration von WillPopScope zu PopScope Bild

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);
  }
}
Kommentare  
Kommentar erstellen
Mit dem Abschicken des Kommentars erklären sie sich mit der in der Datenschutzerklärung dargelegten Datenerhebung für Kommentare einverstanden. Spam, unangebrachte Werbung und andere unerwünschte Inhalte werden entfernt. Das Abonnieren via E-Mail ist nur für E-Mail Adressen erlaubt die Sie rechtmäßig administrieren. Widerrechtliche Abonnements werden entfernt.