Saltar al contenido principal

Atrás predictivo de Android

La capacidad de controlar la navegación hacia atrás en el momento en que se recibe un gesto de retroceso ha sido reemplazada por una API de navegación anticipada (ahead-of-time) para admitir la función Predictive Back de Android 14.

Resumen

#

Para admitir la función Predictive Back de Android 14, un conjunto de APIs de tipo ahead-of-time han reemplazado a las APIs de navegación just-in-time, como WillPopScope y Navigator.willPop.

Contexto

#

Android 14 introdujo la característica Predictive Back, que permite al usuario echar un vistazo detrás de la ruta actual durante un gesto de retroceso válido y decidir si continuar hacia atrás o cancelar el gesto. Esto era incompatible con las APIs de navegación de Flutter que permiten al desarrollador cancelar un gesto de retroceso después de recibirlo.

Con el atrás predictivo, la animación de retroceso comienza inmediatamente cuando el usuario inicia el gesto y antes de que se haya confirmado. No hay oportunidad para que la aplicación de Flutter decida si se permite que suceda en ese momento. Debe saberse de antemano.

Por esta razón, todas las APIs que permiten a un desarrollador de aplicaciones de Flutter cancelar una navegación hacia atrás al momento en que se recibe un gesto de retroceso ahora están obsoletas. Han sido reemplazadas por APIs equivalentes que mantienen un State booleano en todo momento que dicta si la navegación hacia atrás es posible o no. Cuando lo es, la animación de retroceso predictivo ocurre como de costumbre. De lo contrario, la navegación se detiene. En ambos casos, se informa al desarrollador de la aplicación que se intentó un retroceso y si tuvo éxito.

PopScope

#

La clase PopScope reemplaza directamente a WillPopScope para habilitar el atrás predictivo. En lugar de decidir si un pop es posible en el momento en que ocurre, esto se establece de antemano con el booleano canPop. Aún puedes escuchar los pops utilizando onPopInvoked.

dart
PopScope(
  canPop: _myPopDisableEnableLogic(),
  onPopInvoked: (bool didPop) {
    // Handle the pop. If `didPop` is false, it was blocked.
  },
)

Form.canPop y Form.onPopInvoked

#

Estos dos nuevos parámetros se basan en PopScope y reemplazan el parámetro obsoleto Form.onWillPop. Se utilizan con PopScope de la misma manera que la descrita anteriormente.

dart
Form(
  canPop: _myPopDisableEnableLogic(),
  onPopInvoked: (bool didPop) {
    // Handle the pop. If `didPop` is false, it was blocked.
  },
)

Route.popDisposition

#

Este getter devuelve de forma síncrona el RoutePopDisposition para la ruta, que describe cómo se comportarán los pops.

dart
if (myRoute.popDisposition == RoutePopDisposition.doNotPop) {
  // Back gestures are disabled.
}

ModalRoute.registerPopEntry y ModalRoute.unregisterPopEntry

#

Usa estos métodos para registrar Widgets PopScope, que se evaluarán cuando la ruta decida si puede hacer pop. Esta funcionalidad podría usarse al implementar un Widget PopScope personalizado.

dart
@override
void didChangeDependencies() {
  super.didChangeDependencies();
  final ModalRoute<dynamic>? nextRoute = ModalRoute.of(context);
  if (nextRoute != _route) {
    _route?.unregisterPopEntry(this);
    _route = nextRoute;
    _route?.registerPopEntry(this);
  }
}

Guía de migración

#

Migración de WillPopScope a PopScope

#

El reemplazo directo del Widget WillPopScope es el Widget PopScope. En muchos casos, la lógica que se ejecutaba en el momento del gesto de retroceso en onWillPop se puede realizar en tiempo de compilación y establecer en canPop.

Código antes de la migración:

dart
WillPopScope(
  onWillPop: () async {
    return _myCondition;
  },
  child: ...
),

Código después de la migración:

dart
PopScope(
  canPop: _myCondition,
  child: ...
),

Para los casos en los que sea necesario ser notificado de que se intentó un pop, el método onPopInvoked se puede utilizar de manera similar a onWillPop. Ten en cuenta que mientras onWillPop se llamaba antes de que se manejara el pop y tenía la capacidad de cancelarlo, onPopInvoked se llama después de que el pop ha terminado de manejarse.

Código antes de la migración:

dart
WillPopScope(
  onWillPop: () async {
    _myHandleOnPopMethod();
    return true;
  },
  child: ...
),

Código después de la migración:

dart
PopScope(
  canPop: true,
  onPopInvoked: (bool didPop) {
    _myHandleOnPopMethod();
  },
  child: ...
),

Migración de WillPopScope a NavigatorPopHandler para Navigators anidados

#

Un caso de uso muy común de WillPopScope era manejar adecuadamente los gestos de retroceso al usar Widgets Navigator anidados. También es posible hacer esto usando PopScope, pero ahora hay un Widget contenedor que lo hace aún más fácil: NavigatorPopHandler.

Código antes de la migración:

dart
WillPopScope(
  onWillPop: () async => !(await _nestedNavigatorKey.currentState!.maybePop()),
  child: Navigator(
    key: _nestedNavigatorKey,
    
  ),
)

Código después de la migración:

dart
NavigatorPopHandler(
  onPop: () => _nestedNavigatorKey.currentState!.pop(),
  child: Navigator(
    key: _nestedNavigatorKey,
    
  ),
)

Migración de Form.onWillPop a Form.canPop y Form.onPopInvoked

#

Anteriormente, Form utilizaba una instancia de WillPopScope internamente y exponía su método onWillPop. Esto ha sido reemplazado por un PopScope que expone sus métodos canPop y onPopInvoked. La migración es idéntica a la migración de WillPopScope a PopScope, detallada anteriormente.

Migración de Route.willPop a Route.popDisposition

#

El método willPop de Route devolvía un Future<RoutePopDisposition> para adaptarse al hecho de que los pops podían cancelarse. Ahora que eso ya no es así, esta lógica se ha simplificado a un getter síncrono.

Código antes de la migración:

dart
if (await myRoute.willPop() == RoutePopDisposition.doNotPop) {
  ...
}

Código después de la migración:

dart
if (myRoute.popDisposition == RoutePopDisposition.doNotPop) {
  ...
}

Migración de ModalRoute.add/removeScopedWillPopCallback a ModalRoute.(un)registerPopEntry

#

Internamente, ModalRoute realizaba un seguimiento de la existencia de WillPopScopes en su subárbol de Widgets registrándolos con addScopedWillPopCallback y removeScopedWillPopCallback. Dado que PopScope reemplaza a WillPopScope, estos métodos han sido reemplazados por registerPopEntry y unregisterPopEntry, respectivamente.

PopEntry es implementado por PopScope con el fin de exponer solo la información mínima necesaria a ModalRoute. Cualquiera que escriba su propio PopScope debería implementar PopEntry y registrar y desregistrar su Widget con su ModalRoute contenedor.

Código antes de la migración:

dart
@override
void didChangeDependencies() {
  super.didChangeDependencies();
  if (widget.onWillPop != null) {
    _route?.removeScopedWillPopCallback(widget.onWillPop!);
  }
  _route = ModalRoute.of(context);
  if (widget.onWillPop != null) {
    _route?.addScopedWillPopCallback(widget.onWillPop!);
  }
}

Código después de la migración:

dart
@override
void didChangeDependencies() {
  super.didChangeDependencies();
  _route?.unregisterPopEntry(this);
  _route = ModalRoute.of(context);
  _route?.registerPopEntry(this);
}

Migración de ModalRoute.hasScopedWillPopCallback a ModalRoute.popDisposition

#

Este método se utilizaba anteriormente para un caso de uso muy similar a Predictive Back pero en la librería de Cupertino, donde ciertas transiciones de retroceso permitían cancelar la navegación. La transición de ruta se deshabilitaba cuando existía incluso la posibilidad de que un Widget WillPopScope cancelara el pop.

Ahora que la API requiere que esto se decida de antemano, esto ya no necesita basarse especulativamente en la existencia de Widgets PopScope. La lógica definitiva de si un ModalRoute tiene el pop bloqueado por un Widget PopScope está integrada en ModalRoute.popDisposition.

Código antes de la migración:

dart
if (_route.hasScopedWillPopCallback) {
  // Disable predictive route transitions.
}

Código después de la migración:

dart
if (_route.popDisposition == RoutePopDisposition.doNotPop) {
  // Disable predictive route transitions.
}

Migración de un diálogo de confirmación de retroceso

#

A veces se utilizaba WillPopScope para mostrar un diálogo de confirmación cuando se recibía un gesto de retroceso. Esto todavía se puede hacer con PopScope con un patrón similar.

Código antes de la migración:

dart
WillPopScope(
  onWillPop: () async {
    final bool? shouldPop = await _showBackDialog();
    return shouldPop ?? false;
  },
  child: child,
)

Código después de la migración:

dart
return PopScope(
  canPop: false,
  onPopInvoked: (bool didPop) async {
    if (didPop) {
      return;
    }
    final NavigatorState navigator = Navigator.of(context);
    final bool? shouldPop = await _showBackDialog();
    if (shouldPop ?? false) {
      navigator.pop();
    }
  },
  child: child,
)

Soporte para el atrás predictivo

#
  1. Ejecutar Android 14 (nivel de API 34) o superior.
  2. Habilita la flag de función para retroceso predictivo en el dispositivo bajo "Opciones para desarrolladores". Esto no será necesario en versiones futuras de Android.
  3. Establece android:enableOnBackInvokedCallback="true" en android/app/src/main/AndroidManifest.xml. Si es necesario, consulta la guía completa de Android. para migrar aplicaciones de Android y admitir el retroceso predictivo.
  4. Asegúrate de estar utilizando la versión 3.14.0-7.0.pre de Flutter o superior.
  5. Asegúrate de que tu aplicación de Flutter no utilice el Widget WillPopScope. Usarlo deshabilita el retroceso predictivo. Si es necesario, usa PopScope en su lugar.
  6. Ejecuta la aplicación y realiza un gesto de retroceso (desliza desde el lado izquierdo de la pantalla).

Timeline

#

Introducido en la versión: 3.14.0-7.0.pre
En la versión estable: 3.16

Referencias

#

Documentación de la API:

Issues relevantes:

PRs relevantes: