Força o Flutter Navigator a recarregar o estado ao abrir

108

Tenho um StatefulWidgetno Flutter com botão, que me navega para outro StatefulWidgetusando Navigator.push(). No segundo widget, estou mudando o estado global (algumas preferências do usuário). Quando volto do segundo widget para o primeiro, o uso Navigator.pop()do primeiro widget está no estado antigo, mas quero forçar a recarga. Alguma idéia de como fazer isso? Eu tenho uma ideia, mas parece feia:

  1. pop para remover o segundo widget (o atual)
  2. pop novamente para remover o primeiro widget (anterior)
  3. empurre o primeiro widget (deve forçar o redesenho)
Bartektartanus
fonte
1
Sem resposta, apenas um comentário geral: no meu caso, o que me trouxe aqui procurando por isso seria resolvido com apenas um método de sincronização para shared_preferences onde tenho a garantia de receber uma preferência atualizada que escrevi há pouco em outra página . : \ Mesmo usando .então (...) nem sempre me dá os dados atualizados de gravação pendente.
ChrisH de
Acabei de retornar um valor da nova página no pop, resolvi meu problema. Consulte flutter.dev/docs/cookbook/navigation/returning-data
Joe M
Deve ser visto: stackoverflow.com/a/64006691/10563627
Paresh Mangukiya

Respostas:

85

Há algumas coisas que você pode fazer aqui. A resposta de @Mahi enquanto correta poderia ser um pouco mais sucinta e realmente usar push em vez de showDialog, como o OP estava perguntando. Este é um exemplo 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(),
      ),
    );

No entanto, há outra maneira de fazer isso que pode se adequar bem ao seu caso de uso. Se estiver usando o globalcomo algo que afeta a construção de sua primeira página, você pode usar um InheritedWidget para definir suas preferências globais de usuário e, cada vez que elas forem alteradas, sua FirstPage será reconstruída. Isso funciona até mesmo em um widget sem estado, conforme mostrado abaixo (mas também deve funcionar em um widget com estado).

Um exemplo de inheritedWidget em flutter é o tema do aplicativo, embora eles o definam em um widget em vez de construí-lo diretamente, como fiz aqui.

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(),
      ),
    );

Se você usar o widget herdado, não precisará se preocupar em ficar atento ao pop da página que enviou, o que funcionará para casos de uso básicos, mas pode acabar tendo problemas em um cenário mais complexo.

rmtmckenzie
fonte
Excelente, o primeiro caso funcionou perfeitamente para mim (o segundo é perfeito para uma situação mais complexa). Usar OnWillPopUp da segunda página não estava claro para mim; e nem mesmo funcionou.
cdsaenz
Ei, e se eu quiser atualizar meu item de lista. Digamos que minha primeira página contém itens de lista e a segunda página contém detalhes de itens. Estou atualizando o valor do item e quero atualizá-lo novamente nos itens da lista. Como conseguir isso?
Aanal Mehta
@AanalMehta posso dar-lhe uma recomendação rápida - tenha um armazenamento de backend (ou seja, tabela sqflite ou lista na memória) que é usado em ambas as páginas, e no. Então você pode forçar seu widget a atualizar de alguma forma (eu, pessoalmente, usei um contador de incremento que eu alterei em setState antes - não é a solução mais limpa, mas funciona) ... Ou, você pode passar as alterações de volta para a página original na função. então, fazer as modificações em sua lista, e em seguida, reconstrua (mas observe que fazer alterações em uma lista não aciona uma atualização, portanto, use novamente o contador de incremento).
rmtmckenzie
@AanalMehta Mas se isso não ajudar, eu recomendo que você procure algumas outras respostas que podem ser mais relevantes (tenho quase certeza de que vi algumas sobre listas, etc.) ou faça uma nova pergunta.
rmtmckenzie
@rmtmckenzie Na verdade, tentei a solução acima. Eu apliquei as alterações em .então, mas não será refletido. Acho que agora tenho apenas uma opção de usar provedores. De qualquer forma, muito obrigado pela ajuda.
Aanal Mehta
20

Existem 2 coisas, passando dados de

  • 1ª página para 2ª

    Use isto na primeira página

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

    Use isto na 2ª página.

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

  • 2ª página à 1ª

    Use isto na 2ª página

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

    Use isto na primeira página, é o mesmo que foi usado anteriormente, mas com poucas modificações.

    // receiving "Bar" in 1st
    String received = await Navigator.push(context, MaterialPageRoute(builder: (_) => Page2("Foo")));
    
CopsOnRoad
fonte
Como posso recarregar a rota A ao sair da rota C usando o método Navigator.popUntil.
Vinoth Vino
1
@VinothVino Ainda não há uma maneira direta de fazer isso; você precisa fazer algum tipo de solução alternativa.
CopsOnRoad
@CopsOnRoad na atividade TabBar para chamar a segunda guia do botão de envio da caixa de diálogo, clique em Navigator.pushreplacement para redirecionar para a segunda guia.
sj
13

O truque fácil é usar o Navigator.pushReplacement método

Página 1

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

Página 2

Navigator.pushReplacement(
  context,
  MaterialPageRoute(
    builder: (context) => Page1(),
  ),
);
Deus oficial
fonte
Desta forma, você está perdendo a pilha do navegador. Que tal abrir a tela com o botão Voltar?
encubos
11

Você pode usar pushReplacement e especificar a nova rota

Sobhan Jachuck
fonte
1
Mas você está perdendo a pilha do navegador. E se você abrir a tela usando o botão Voltar do Android?
encubos
5

Este trabalho é muito bom, eu peguei deste documento da página flutter : flutter doc

Eu defini o método para controlar a navegação da primeira 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(() {});
    }
  }

Então, eu o chamo em um método de ação de um tinteiro, mas também pode ser chamado de um botão:

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

E finalmente na segunda página, para retornar algo (eu retornei um bool, você pode retornar o que quiser):

onTap: () {
  Navigator.pop(context, true);
}
Pedro Molina
fonte
1
Você pode até mesmo await Navigator....omitir um valor de resultado no pop.
0llie
5

Para mim, isso parece funcionar:

Navigator.of(context).pushNamed("/myRoute").then((value) => setState(() {}));

Em seguida, basta chamar Navigator.pop()a criança.

marre
fonte
4

Coloque isso onde você está empurrando para a segunda tela (dentro de uma função assíncrona)

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

Coloque isso onde você está colocando

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

O setStateé chamado dentro do popfechamento para atualizar os dados.

Mathew Varghese
fonte
2
Onde exatamente você passa setStatecomo argumento? Você está basicamente ligando setStatedentro de um encerramento. Não passando isso como um argumento.
Volkan Güven
Esta é a resposta exata que eu estava procurando. Basicamente, eu queria um manipulador de conclusão para Navigator.pop.
David Chopin,
3

minha solução foi adicionar um parâmetro de função no SecondPage, depois receber a função de recarregamento que está sendo feita no FirstPage e executar a função antes da linha Navigator.pop (contexto).

Primeira página

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

em seguida, avançando para a próxima página ...

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

SecondPage

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

em seguida, antes da linha pop do navegador,

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

Tudo o que precisa ser recarregado da página anterior deve ser atualizado após o pop.

Rodalyn Camba
fonte
1

Você pode devolver a dynamic resultquando estiver abrindo o contexto e, em seguida, chamar setState((){})quando o valor for, truecaso contrário, apenas deixe o estado como está.

Eu colei alguns trechos de código para sua referência.

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;
}

Atenciosamente, Mahi

Mahi
fonte
Não tenho certeza se entendi como fazer a segunda parte. A primeira é simplesmente Navigator.pop(context, true), certo? Mas como posso obter esse truevalor? Usando BuildContext?
bartektartanus
Não está funcionando para mim. Usei o resultado do push, chamado setState e tenhosetState() 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, isso é interessante, você usou if(!mounted) return;antes de cada chamada para setState((){});evitar a atualização dos widgets descartados ou que não estão mais ativos.
Mahi de
Sem erro, mas também não está funcionando como eu previa;)
bartektartanus
Tenho o mesmo problema que @bartektartanus. Se eu adicionar if! Montado antes de setState, meu estado nunca será definido. Parece simples, mas não descobri depois de algumas horas.
Swift de
1

Necessário para forçar a reconstrução de um dos meus widgets sem estado. Não queria usar stateful. Surgiu com esta solução:

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

Observe que context e enclosingWidgetContext podem ser contextos iguais ou diferentes. Se, por exemplo, você enviar de dentro do StreamBuilder, eles serão diferentes.

Não fazemos nada aqui com o ModalRoute. O ato de assinar sozinho é suficiente para forçar a reconstrução.

CKK
fonte
1

Se estiver usando uma caixa de diálogo de alerta, você pode usar um Futuro que é concluído quando a caixa de diálogo é descartada. Após a conclusão do futuro, você pode forçar o widget a recarregar o estado.

Primeira página

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

Na caixa de diálogo de alerta

Navigator.of(context).pop();
Nimna Perera
fonte
0

Hoje enfrentei a mesma situação, mas consegui resolvê-la de uma maneira muito mais fácil, apenas defini uma variável global que foi usada na primeira classe com estado, e quando navego para um segundo widget com estado, deixo ele atualizar o valor do global variável que força automaticamente o primeiro widget a ser atualizado. Aqui está um exemplo (eu o escrevi com pressa, então não coloquei um andaime ou um aplicativo de material, eu só queria ilustrar meu ponto):

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++ ;
         },
       ),
     }
Mazin Ibrahim
fonte
4
Alterar a variável não reconstruirá automaticamente o widget. Depois Navigator.pop()ele manterá o estado antes Navigator.push()até setStateser chamado novamente, o que não acontece neste código. Estou esquecendo de algo?
Ricardo BRGWeb
0

Este código simples funcionou para eu ir à raiz e recarregar o estado:

    ...
    onPressed: () {
         Navigator.of(context).pushNamedAndRemoveUntil('/', ModalRoute.withName('/'));
                },
    ...
Juanma Menendez
fonte
0

Resumindo, você deve fazer o widget observar o estado. Você precisa de gerenciamento de estado para isso.

Meu método é baseado no Provider explicado em Flutter Architecture Samples e também em Flutter Docs . Consulte-os para uma explicação mais concisa, mas mais ou menos as etapas são:

  • Defina seu modelo de estado com os estados que o widget precisa observar.

Você poderia ter vários estados dizendo datae isLoading, para aguardar algum processo de API. O próprio modelo se estende ChangeNotifier.

  • Envolva os widgets que dependem desses estados com a classe do inspetor.

Isso pode ser Consumerou Selector.

  • Quando você precisa "recarregar", basicamente atualiza esses estados e transmite as alterações.

Para o modelo de estado, a classe seria mais ou menos como segue. Preste atenção em notifyListenersquem transmite as mudanças.

class DataState extends ChangeNotifier{

  bool isLoading;
  
  Data data;

  Future loadData(){
    isLoading = true;
    notifyListeners();

    service.get().then((newData){
      isLoading = false;
      data = newData;
      notifyListeners();
    });
  }
  
}

Agora, para o widget. Este será um código-esqueleto.

return ChangeNotifierProvider(

  create: (_) => DataState()..loadData(),
      
  child: ...{
    Selector<DataState, bool>(

        selector: (context, model) => model.isLoading,

        builder: (context, isLoading, _) {
          if (isLoading) {
            return ProgressBar;
          }

          return Container(

              child: Consumer<DataState>(builder: (context, dataState, child) {

                 return WidgetData(...);

              }
          ));
        },
      ),
  }
);

A instância do modelo de estado é fornecida por ChangeNotifierProvider. O Seletor e o Consumidor observam os estados, cada um para isLoadinge datarespectivamente. Não há muita diferença entre eles, mas pessoalmente como você os usa depende do que seus construtores fornecem. O consumidor fornece acesso ao modelo de estado, assim chamandoloadData é mais simples para quaisquer widgets diretamente abaixo dele.

Se não, então você pode usar Provider.of. Se quisermos atualizar a página ao retornar da segunda tela, podemos fazer algo assim:

await Navigator.push(context, 
  MaterialPageRoute(
    builder: (_) {
     return Screen2();
));

Provider.of<DataState>(context, listen: false).loadData();

no mito
fonte
0

Para mim funcionou:

...
onPressed: (){pushUpdate('/somePageName');}
...

pushUpdate (string pageName) async {      //in the same class
  await pushPage(context, pageName);
  setState(() {});
}


//---------------------------------------------
//general sub
pushPage (context, namePage) async {
  await Navigator.pushNamed(context, namePage);
}

Neste caso, não importa como você pop (com o botão na IU ou "voltar" no android), a atualização será feita.

Logic2Paradigm
fonte