Forzar al navegador Flutter a recargar el estado al hacer estallar


96

Tengo uno StatefulWidgeten Flutter con botón, que me lleva a otro StatefulWidgetusando Navigator.push(). En el segundo widget, estoy cambiando el estado global (algunas preferencias del usuario). Cuando vuelvo del segundo widget al primero, Navigator.pop()el primer widget está en estado antiguo, pero quiero forzar su recarga. ¿Alguna idea de cómo se hace esto? Tengo una idea pero se ve fea:

  1. pop para eliminar el segundo widget (el actual)
  2. pop de nuevo para eliminar el primer widget (el anterior)
  3. empujar el primer widget (debería forzar el redibujo)

1
Sin respuesta, solo un comentario general: en mi caso, lo que me trajo aquí buscando esto se resolvería con solo tener un método de sincronización para shared_preferences donde tengo la garantía de recuperar una preferencia actualizada que escribí hace un momento en otra página . : \ Incluso usando .then (...) no siempre obtengo los datos actualizados de escritura pendiente.
ChrisH

Acabo de devolver un valor de la nueva página en la ventana emergente, resolvió mi problema. Consulte flutter.dev/docs/cookbook/navigation/returning-data
Joe M

Respuestas:


77

Hay un par de cosas que podrías hacer aquí. La respuesta de @ Mahi, aunque correcta, podría ser un poco más concisa y, en realidad, usar push en lugar de showDialog como preguntaba el OP. Este es un ejemplo que usa Navigator.push:

import 'package:flutter/material.dart';

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Container(
      color: Colors.green,
      child: new Column(
        children: <Widget>[
          new RaisedButton(
            onPressed: () => Navigator.pop(context),
            child: new Text("back"),
          ),
        ],
      ),
    );
  }
}

class FirstPage extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => new FirstPageState();
}

class FirstPageState extends State<FirstPage> {

  Color color = Colors.white;

  @override
  Widget build(BuildContext context) {
    return new Container(
      color: color,
      child: new Column(
        children: <Widget>[
          new RaisedButton(
              child: new Text("next"),
              onPressed: () {
                Navigator
                    .push(
                  context,
                  new MaterialPageRoute(builder: (context) => new SecondPage()),
                )
                    .then((value) {
                  setState(() {
                    color = color == Colors.white ? Colors.grey : Colors.white;
                  });
                });
              }),
        ],
      ),
    );
  }
}

void main() => runApp(
      new MaterialApp(
        builder: (context, child) => new SafeArea(child: child),
        home: new FirstPage(),
      ),
    );

Sin embargo, hay otra forma de hacer esto que podría adaptarse bien a su caso de uso. Si está usando el globalcomo algo que afecta la construcción de su primera página, puede usar un InheritedWidget para definir sus preferencias de usuario globales, y cada vez que se modifiquen, su FirstPage se reconstruirá. Esto incluso funciona dentro de un widget sin estado como se muestra a continuación (pero también debería funcionar en un widget con estado).

Un ejemplo de heritageWidget en flutter es el tema de la aplicación, aunque lo definen dentro de un widget en lugar de tenerlo directamente compilado como lo he hecho aquí.

import 'package:flutter/material.dart';
import 'package:meta/meta.dart';

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Container(
      color: Colors.green,
      child: new Column(
        children: <Widget>[
          new RaisedButton(
            onPressed: () {
              ColorDefinition.of(context).toggleColor();
              Navigator.pop(context);
            },
            child: new Text("back"),
          ),
        ],
      ),
    );
  }
}

class ColorDefinition extends InheritedWidget {
  ColorDefinition({
    Key key,
    @required Widget child,
  }): super(key: key, child: child);

  Color color = Colors.white;

  static ColorDefinition of(BuildContext context) {
    return context.inheritFromWidgetOfExactType(ColorDefinition);
  }

  void toggleColor() {
    color = color == Colors.white ? Colors.grey : Colors.white;
    print("color set to $color");
  }

  @override
  bool updateShouldNotify(ColorDefinition oldWidget) =>
      color != oldWidget.color;
}

class FirstPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    var color = ColorDefinition.of(context).color;

    return new Container(
      color: color,
      child: new Column(
        children: <Widget>[
          new RaisedButton(
              child: new Text("next"),
              onPressed: () {
                Navigator.push(
                  context,
                  new MaterialPageRoute(builder: (context) => new SecondPage()),
                );
              }),
        ],
      ),
    );
  }
}

void main() => runApp(
      new MaterialApp(
        builder: (context, child) => new SafeArea(
              child: new ColorDefinition(child: child),
            ),
        home: new FirstPage(),
      ),
    );

Si usa un widget heredado, no tiene que preocuparse por ver la ventana emergente de la página que presionó, que funcionará para casos de uso básicos, pero puede terminar teniendo problemas en un escenario más complejo.


Excelente, el primer caso me funcionó a la perfección (el segundo es perfecto para una situación más compleja). El uso de OnWillPopUp desde la segunda página no me quedó claro; y ni siquiera funcionó en absoluto.
cdsaenz

Oye, ¿qué pasa si quiero actualizar el elemento de mi lista? Digamos que mi primera página contiene elementos de lista y la segunda página contiene detalles del elemento. Estoy actualizando el valor del artículo y quiero que se actualice nuevamente en los artículos de la lista. ¿Cómo lograrlo?
Aanal Mehta

@AanalMehta Puedo darle una recomendación rápida: tenga una tienda de backend (es decir, una tabla sqflite o una lista en memoria) que se use en ambas páginas, y en el .Entonces, podría forzar a su widget a actualizar de alguna manera (yo personalmente he usado un contador incremental que cambié en setState antes; no es la solución más limpia pero funciona) ... O bien, puede pasar los cambios a la página original en la función .then, hacer las modificaciones a su lista y luego reconstruir (pero tenga en cuenta que hacer cambios en una lista no desencadena una actualización, por lo que una vez más use el contador incremental).
rmtmckenzie

@AanalMehta Pero si eso no ayuda, le recomiendo que busque otras respuestas que puedan ser más relevantes (estoy bastante seguro de que he visto algunas sobre listas, etc.), o que haga una nueva pregunta.
rmtmckenzie

@rmtmckenzie En realidad, probé la solución anterior. Apliqué los cambios en .entonces pero no se reflejan. Supongo que ahora solo tengo una opción para usar proveedores. De todos modos, muchas gracias por tu ayuda.
Aanal Mehta

18

Hay dos cosas, pasar datos de

  • 1ra página a 2da

    Use esto en la primera página

    // sending "Foo" from 1st
    Navigator.push(context, MaterialPageRoute(builder: (_) => Page2("Foo")));
    

    Use esto en la segunda página.

    class Page2 extends StatelessWidget {
      final String string;
    
      Page2(this.string); // receiving "Foo" in 2nd
    
      ...
    }
    

  • 2da página a 1ra

    Use esto en la segunda página

    // sending "Bar" from 2nd
    Navigator.pop(context, "Bar");
    

    Use esto en la 1ra página, es el mismo que se usó anteriormente pero con pocas modificaciones.

    // receiving "Bar" in 1st
    String received = await Navigator.push(context, MaterialPageRoute(builder: (_) => Page2("Foo")));
    

¿Cómo puedo recargar la ruta A cuando salgo de la ruta C usando el método Navigator.popUntil?
Vinoth Vino

1
@VinothVino No hay una forma directa de hacerlo todavía, necesita hacer algún tipo de solución.
CopsOnRoad

10

Puede utilizar pushReplacement y especificar la nueva ruta


Pero estás perdiendo la pila del navegador. ¿Qué pasa si abre la pantalla con el botón Atrás de Android?
encubos

10

El truco fácil es utilizar el método Navigator.pushReplacement

Página 1

Navigator.pushReplacement(
  context,
  MaterialPageRoute(
    builder: (context) => Page2(),
  ),
);

Página 2

Navigator.pushReplacement(
  context,
  MaterialPageRoute(
    builder: (context) => Page1(),
  ),
);

De esta forma estás perdiendo la pila del navegador. ¿Qué hay de hacer estallar la pantalla con el botón Atrás?
encubos

5

Este trabajo es realmente bueno, lo obtuve de este documento de la página de flutter : flutter doc

Definí el método para controlar la navegación desde la primera página.

_navigateAndDisplaySelection(BuildContext context) async {
    final result = await Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => AddDirectionPage()),
    );

    //below you can get your result and update the view with setState
    //changing the value if you want, i just wanted know if i have to  
    //update, and if is true, reload state

    if (result) {
      setState(() {});
    }
  }

Entonces, lo llamo en un método de acción desde un tintero, pero también se puede llamar desde un botón:

onTap: () {
   _navigateAndDisplaySelection(context);
},

Y finalmente en la segunda página, para devolver algo (devolví un bool, puedes devolver lo que quieras):

onTap: () {
  Navigator.pop(context, true);
}

1
Incluso puede solo await Navigator....y omitir un valor de resultado en pop.
0llie

4

Coloque esto donde está presionando a la segunda pantalla (dentro de una función asincrónica)

Function f;
f= await Navigator.pushNamed(context, 'ScreenName');
f();

Pon esto donde estás haciendo estallar

Navigator.pop(context, () {
 setState(() {});
});

Se setStatellama dentro del popcierre para actualizar los datos.


2
¿Dónde pasa exactamente setStatecomo argumento? Básicamente estás llamando setStatedentro de un cierre. No pasarlo como argumento.
Volkan Güven

Esta es la respuesta exacta que estaba buscando. Básicamente quería un controlador de finalización para Navigator.pop.
David Chopin

2

mi solución fue agregando un parámetro de función en SecondPage, luego recibió la función de recarga que se está haciendo desde FirstPage, luego ejecutó la función antes de la línea Navigator.pop (contexto).

Primera página

refresh() {
setState(() {
//all the reload processes
});
}

luego, al pasar a la página siguiente ...

Navigator.push(context, new MaterialPageRoute(builder: (context) => new SecondPage(refresh)),);

Segunda pagina

final Function refresh;
SecondPage(this.refresh); //constructor

luego, antes de la línea emergente del navegador,

widget.refresh(); // just refresh() if its statelesswidget
Navigator.pop(context);

Todo lo que necesita ser recargado desde la página anterior debe actualizarse después del pop.


1

Puede devolver un dynamic resultcuando está abriendo el contexto y luego llamar al setState((){})cuando el valor es; de lo truecontrario, deje el estado como está.

He pegado algunos fragmentos de código para su referencia.

handleClear() async {
    try {
      var delete = await deleteLoanWarning(
        context,
        'Clear Notifications?',
        'Are you sure you want to clear notifications. This action cannot be undone',
      );
      if (delete.toString() == 'true') {
        //call setState here to rebuild your state.

      }
    } catch (error) {
      print('error clearing notifications' + error.toString());
             }
  }



Future<bool> deleteLoanWarning(BuildContext context, String title, String msg) async {

  return await showDialog<bool>(
        context: context,
        child: new AlertDialog(
          title: new Text(
            title,
            style: new TextStyle(fontWeight: fontWeight, color: CustomColors.continueButton),
            textAlign: TextAlign.center,
          ),
          content: new Text(
            msg,
            textAlign: TextAlign.justify,
          ),
          actions: <Widget>[
            new Container(
              decoration: boxDecoration(),
              child: new MaterialButton(
                child: new Text('NO',),
                onPressed: () {
                  Navigator.of(context).pop(false);
                },
              ),
            ),
            new Container(
              decoration: boxDecoration(),
              child: new MaterialButton(
                child: new Text('YES', ),
                onPressed: () {
                  Navigator.of(context).pop(true);
                },
              ),
            ),
          ],
        ),
      ) ??
      false;
}

Saludos, Mahi


No estoy seguro de entender cómo hacer la segunda parte. Primero es simplemente Navigator.pop(context, true), ¿verdad? Pero, ¿cómo puedo obtener este truevalor? Usando BuildContext?
bartektartanus

No funciona para mi. He usado el resultado de push, llamado setState y obtuvesetState() called after dispose(): This error happens if you call setState() on a State object for a widget that no longer appears in the widget tree (e.g., whose parent widget no longer includes the widget in its build). This error can occur when code calls setState() from a timer or an animation callback. The preferred solution is to cancel the timer or stop listening to the animation in the dispose() callback. ....
bartektartanus

hmm, esto es interesante si lo usó if(!mounted) return;antes de cada llamada de setState((){});esta manera puede evitar actualizar los widgets eliminados o los widgets que ya no están activos.
Mahi

No hay error, pero tampoco funciona como predije;)
bartektartanus

Tengo el mismo problema que @bartektartanus. Si agrego if! Installed antes de setState, mi estado nunca se establecerá. Parece simple, pero no lo he descubierto después de unas horas.
Swift

1

Necesitaba forzar la reconstrucción de uno de mis widgets sin estado. No quería usar stateful. Se me ocurrió esta solución:

await Navigator.of(context).pushNamed(...);
ModalRoute.of(enclosingWidgetContext);

Tenga en cuenta que el contexto y el WidgetContext adjunto podrían ser el mismo o diferentes contextos. Si, por ejemplo, empuja desde dentro de StreamBuilder, serían diferentes.

No hacemos nada aquí con ModalRoute. El solo hecho de suscribirse es suficiente para forzar la reconstrucción.


1

Si está utilizando un cuadro de diálogo de alerta, puede usar un Futuro que se completa cuando se cierra el cuadro de diálogo. Después de la finalización del futuro, puede forzar al widget a recargar el estado.

Primera página

onPressed: () async {
    await showDialog(
       context: context,
       builder: (BuildContext context) {
            return AlertDialog(
                 ....
            );
       }
    );
    setState(() {});
}

En el diálogo de alerta

Navigator.of(context).pop();

0

Hoy me enfrenté a la misma situación pero logré resolverla de una manera mucho más fácil, acabo de definir una variable global que se usó en la primera clase con estado, y cuando navego a un segundo widget con estado dejo que actualice el valor del global variable que fuerza automáticamente la actualización del primer widget. Aquí hay un ejemplo (lo escribí con prisa, así que no puse un andamio o una aplicación de material, solo quería ilustrar mi punto):

import 'package:flutter/material.dart';
int count = 0 ;

class FirstPage extends StatefulWidget {
FirstPage({Key key}) : super(key: key);

@override
_FirstPageState createState() => _FirstPageState();
}

class _FirstPageState extends State<FirstPage> {
@override
Widget build(BuildContext context) {
return InkWell(
onTap(){
Navigator.of(context).push(MaterialPageRoute(builder: (context) =>
                  new SecondPage());
},
child: Text('First', style : TextStyle(fontSize: count == 0 ? 20.0 : 12.0)),
),

}


class SecondPage extends StatefulWidget {
SecondPage({Key key}) : super(key: key);

@override
_SecondPageState createState() => _SecondPageState();
}

class _SecondPageState extends State<SecondPage> {
@override
Widget build(BuildContext context) {
return IconButton(
         icon: new Icon(Icons.add),
         color: Colors.amber,
         iconSize: 15.0,
         onPressed: (){
         count++ ;
         },
       ),
     }

4
Cambiar la variable no reconstruirá automáticamente el widget. Después Navigator.pop(), mantendrá el estado anterior Navigator.push()hasta que setStatese vuelva a llamar, lo que no sucede en este código. ¿Me estoy perdiendo de algo?
Ricardo BRGWeb

0

Este código simple me funcionó para ir a la raíz y recargar el estado:

    ...
    onPressed: () {
         Navigator.of(context).pushNamedAndRemoveUntil('/', ModalRoute.withName('/'));
                },
    ...
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.