Zmusza nawigatora Flutter do przeładowania stanu podczas wyświetlania

109

Mam jeden StatefulWidgetw Flutter z przyciskiem, który przenosi mnie do drugiego StatefulWidgetza pomocą Navigator.push(). Na drugim widżecie zmieniam stan globalny (niektóre preferencje użytkownika). Kiedy wracam z drugiego widżetu do pierwszego, używanie Navigator.pop()pierwszego widżetu jest w starym stanie, ale chcę wymusić jego ponowne załadowanie. Masz jakiś pomysł, jak to zrobić? Mam jeden pomysł, ale wygląda brzydko:

  1. pop, aby usunąć drugi widżet (bieżący)
  2. wyskocz ponownie, aby usunąć pierwszy widżet (poprzedni)
  3. push pierwszy widget (powinien wymusić przerysowanie)
bartektartanus
źródło
1
Brak odpowiedzi, tylko ogólny komentarz: w moim przypadku to, co przywiodło mnie tutaj, szukając tego, można rozwiązać za pomocą metody synchronizacji dla shared_preferences, gdzie mam gwarancję odzyskania zaktualizowanego pref, który napisałem chwilę temu na innej stronie . : \ Nawet użycie .then (...) nie zawsze daje mi zaktualizowane dane oczekujące na zapis.
ChrisH
Właśnie zwróciłem wartość z nowej strony na pop, rozwiązałem mój problem. Odnieś się do flutter.dev/docs/cookbook/navigation/returning-data
Joe M
Trzeba to zobaczyć: stackoverflow.com/a/64006691/10563627
Paresh Mangukiya

Odpowiedzi:

85

Możesz tutaj zrobić kilka rzeczy. Odpowiedź @ Mahi, chociaż poprawna, mogłaby być nieco bardziej zwięzła i faktycznie używać wypychania zamiast showDialog, o co pytał OP. Oto przykład wykorzystujący 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(),
      ),
    );

Jest jednak inny sposób na zrobienie tego, który może dobrze pasować do twojego przypadku użycia. Jeśli używasz globaljako czegoś, co wpływa na kompilację Twojej pierwszej strony, możesz użyć InheritedWidget, aby zdefiniować swoje globalne preferencje użytkownika, a za każdym razem, gdy zostaną one zmienione, Twoja FirstPage zostanie odbudowana. Działa to nawet w widgecie bezstanowym, jak pokazano poniżej (ale powinno również działać w widgecie stanowym).

Przykładem inheritedWidget in flutter jest motyw aplikacji, chociaż definiują go w widżecie, zamiast budować go bezpośrednio, tak jak tutaj.

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

Jeśli używasz dziedziczonego widżetu, nie musisz się martwić o wypatrywanie popchniętej strony, która będzie działać w podstawowych przypadkach użycia, ale może skończyć się problemami w bardziej złożonym scenariuszu.

rmtmckenzie
źródło
Doskonale, pierwszy przypadek działał u mnie idealnie (drugi jest idealny do bardziej złożonej sytuacji). Korzystanie z OnWillPopUp z drugiej strony nie było dla mnie jasne; i nawet nie działał.
cdsaenz
Hej, co jeśli chcę zaktualizować moją pozycję na liście. Powiedzmy, że moja pierwsza strona zawiera pozycje listy, a druga strona zawiera szczegóły pozycji. Aktualizuję wartość pozycji i chcę, aby z powrotem była aktualizowana na pozycjach na liście. Jak to osiągnąć?
Aanal Mehta
@AanalMehta Mogę dać ci szybką rekomendację - albo mam magazyn zaplecza (tj. Tabelę sqflite lub listę w pamięci), który jest używany z obu stron, a następnie możesz zmusić widżet do odświeżenia w jakiś sposób (ja osobiście używałeś licznika inkrementującego, który wcześniej zmieniłem w setState - nie jest to najczystsze rozwiązanie, ale działa) ... Lub możesz przekazać zmiany z powrotem do oryginalnej strony w funkcji .then, wprowadzić modyfikacje do swojej listy i następnie odbuduj (ale pamiętaj, że wprowadzanie zmian w liście nie wyzwala odświeżania, więc ponownie użyj licznika zwiększającego).
rmtmckenzie
@AanalMehta Ale jeśli to nie pomoże, radzę poszukać innych odpowiedzi, które mogą być bardziej odpowiednie (jestem pewien, że widziałem kilka na temat list itp.) Lub zadać nowe pytanie.
rmtmckenzie
@rmtmckenzie Właściwie wypróbowałem powyższe rozwiązanie. Zastosowałem zmiany w. Potem, ale nie zostaną one odzwierciedlone. Myślę, że teraz mam tylko jedną opcję korzystania z dostawców. W każdym razie bardzo dziękuję za pomoc.
Aanal Mehta
20

Są dwie rzeczy, z których przekazywane są dane

  • Pierwsza strona do drugiej

    Użyj tego na pierwszej stronie

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

    Użyj tego na drugiej stronie.

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

  • 2. strona do 1. strony

    Użyj tego na drugiej stronie

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

    Użyj tego na pierwszej stronie, jest to ten sam, który był używany wcześniej, ale z niewielkimi modyfikacjami.

    // receiving "Bar" in 1st
    String received = await Navigator.push(context, MaterialPageRoute(builder: (_) => Page2("Foo")));
    
CopsOnRoad
źródło
Jak mogę ponownie załadować trasę A podczas wyskakiwania z trasy C przy użyciu metody Navigator.popUntil.
Vinoth Vino
1
@VinothVino Nie ma jeszcze bezpośredniego sposobu, aby to zrobić, musisz zastosować jakieś obejście.
CopsOnRoad
@CopsOnRoad w pasku aktywności TabBar, aby wywołać drugą kartę z okna dialogowego przycisku przesyłania, kliknij z Navigator.pushreplacement, aby przekierować do drugiej karty.
sj
13

Łatwa Sztuczka polega na użyciu Navigator.pushReplacement metody

Strona 1

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

Strona 2

Navigator.pushReplacement(
  context,
  MaterialPageRoute(
    builder: (context) => Page1(),
  ),
);
Oficjalnie Boże
źródło
W ten sposób tracisz stos nawigatora. A co z wyświetlaniem ekranu przyciskiem Wstecz?
encubos
11

Możesz użyć pushReplacement i określić nową trasę

Sobhan Jachuck
źródło
1
Ale tracisz stos nawigatora. Co się stanie, jeśli wyskoczy ekran za pomocą przycisku Wstecz w systemie Android?
encubos
6

Ta praca jest naprawdę dobra, dostałem z tego dokumentu z flutter page: flutter doc

Zdefiniowałem metodę sterowania nawigacją od pierwszej strony.

_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(() {});
    }
  }

Tak więc nazywam to metodą akcji z kałamarza, ale można ją również wywołać z przycisku:

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

I wreszcie na drugiej stronie, aby coś zwrócić (zwróciłem wartość bool, możesz zwrócić, co chcesz):

onTap: () {
  Navigator.pop(context, true);
}
Pedro Molina
źródło
1
Możesz nawet po prostu await Navigator....pominąć wartość wynikową w pop.
0llie
5

Wydaje mi się, że to działa:

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

Następnie po prostu wezwij Navigator.pop()dziecko.

marre
źródło
4

Umieść to tam, gdzie pchasz do drugiego ekranu (wewnątrz funkcji asynchronicznej)

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

Umieść to tam, gdzie strzelasz

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

setStateNazywa się wewnątrz popzamknięcia, aby zaktualizować dane.

Mathew Varghese
źródło
2
Gdzie dokładnie podajesz setStateargument? W zasadzie dzwonisz setStatewewnątrz zamknięcia. Nie podając tego jako argumentu.
Volkan Güven
To jest dokładna odpowiedź, której szukałem. Zasadniczo chciałem mieć obsługi kompletacji Navigator.pop.
David Chopin
3

Moje rozwiązanie polegało na dodaniu parametru funkcji na SecondPage, a następnie odebraniu funkcji przeładowania, która jest wykonywana z FirstPage, a następnie wykonaniu funkcji przed wierszem Navigator.pop (kontekst).

Pierwsza strona

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

następnie przechodząc do następnej strony ...

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

SecondPage

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

następnie przed wierszem pop nawigatora,

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

Wszystko, co trzeba przeładować z poprzedniej strony, powinno zostać zaktualizowane po wyskakującym okienku.

rodalyn camba
źródło
1

Możesz przekazać z powrotem a, dynamic resultgdy otwierasz kontekst, a następnie wywołać, setState((){})gdy wartość jest w trueprzeciwnym razie, po prostu pozostaw stan taki, jaki jest.

Wkleiłem kilka fragmentów kodu w celach informacyjnych.

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

Pozdrawiam, Mahi

Mahi
źródło
Nie jestem pewien, czy rozumiem, jak wykonać drugą część. Po pierwsze, po prostu Navigator.pop(context, true), prawda? Ale jak mogę uzyskać tę truewartość? Używasz BuildContext?
bartektartanus
Nie działa dla mnie. Skorzystałem z wyniku push, zadzwoniłem do setState i otrzymałemsetState() 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, ciekawe, czy używałeś go if(!mounted) return;przed każdym wywołaniem, w setState((){});ten sposób możesz uniknąć aktualizacji usuniętych widżetów lub widżetów, które nie są już aktywne.
Mahi
Żaden błąd, ale też nie działa tak, jak przewidywałem;)
bartektartanus
Mam ten sam problem co @bartektartanus. Jeśli dodam if! Zamontowane przed setState, mój stan nigdy nie zostanie ustawiony. Wydaje się to proste, ale po kilku godzinach nie doszedłem do tego.
Swift
1

Potrzebne do wymuszenia przebudowy jednego z moich bezstanowych widżetów. Nie chciałem używać stanowych. Przyszło z tym rozwiązaniem:

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

Zauważ, że kontekst i otaczający go WidgetContext mogą być tym samym lub różnymi kontekstami. Jeśli na przykład wypchniesz z wnętrza StreamBuilder, będą one inne.

Nic tu nie robimy z ModalRoute. Sama subskrypcja wystarczy, aby wymusić odbudowę.

CKK
źródło
1

Jeśli używasz okna dialogowego z ostrzeżeniem, możesz użyć opcji Przyszłość, która kończy się po zamknięciu okna dialogowego. Po zakończeniu przyszłości możesz zmusić widget do ponownego załadowania stanu.

Pierwsza strona

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

W oknie dialogowym Alert

Navigator.of(context).pop();
Nimna Perera
źródło
0

Dzisiaj miałem taką samą sytuację, ale udało mi się ją rozwiązać w dużo łatwiejszy sposób, właśnie zdefiniowałem zmienną globalną, która była używana w pierwszej klasie stanowej, a kiedy przechodzę do drugiego widgetu stanowego, pozwoliłem jej zaktualizować wartość globalnej zmienna, która automatycznie wymusza aktualizację pierwszego widżetu. Oto przykład (napisałem go w pośpiechu, więc nie umieściłem rusztowania ani aplikacji materiałowej, chciałem tylko zilustrować mój punkt widzenia):

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
źródło
4
Zmiana zmiennej nie spowoduje automatycznego odbudowania widżetu. Po Navigator.pop()to zachować stan sprzed Navigator.push()momentu setStatenazywa się ponownie, co nie zdarza się w tym kodzie. Czy coś mi brakuje?
Ricardo BRGWeb
0

Ten prosty kod zadziałał, aby przejść do katalogu głównego i ponownie załadować stan:

    ...
    onPressed: () {
         Navigator.of(context).pushNamedAndRemoveUntil('/', ModalRoute.withName('/'));
                },
    ...
Juanma Menendez
źródło
0

Krótko mówiąc, powinieneś sprawić, by widżet obserwował stan. Potrzebujesz do tego zarządzania stanem.

Moja metoda jest oparta na dostawcy opisanym w przykładach architektury Flutter oraz Flutter Docs . Zapoznaj się z nimi, aby uzyskać bardziej zwięzłe wyjaśnienia, ale mniej więcej kroki są następujące:

  • Zdefiniuj model stanu za pomocą stanów, które widżet musi obserwować.

Możesz mieć wiele stanów powiedzieć datai isLoadingpoczekać na jakiś proces API. Sam model się rozciąga ChangeNotifier.

  • Otocz widżety, które zależą od tych stanów, klasą watcher.

Może to być Consumerlub Selector.

  • Kiedy potrzebujesz "przeładować", po prostu aktualizujesz te stany i rozgłaszasz zmiany.

Dla modelu stanu klasa wyglądałaby mniej więcej następująco. Zwróć uwagę, notifyListenersktóry rozgłasza zmiany.

class DataState extends ChangeNotifier{

  bool isLoading;
  
  Data data;

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

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

Teraz do widgetu. To będzie bardzo szkieletowy kod.

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

              }
          ));
        },
      ),
  }
);

Wystąpienie modelu stanu jest dostarczane przez ChangeNotifierProvider. Selektor i Konsument obserwują stany, odpowiednio dla isLoadingi data. Nie ma między nimi dużej różnicy, ale osobiście sposób ich użycia zależy od tego, co zapewniają ich konstruktorzy. Konsument zapewnia dostęp do modelu stanu, więc wywoływanie loadDatajest prostsze dla wszystkich widżetów znajdujących się bezpośrednio pod nim.

Jeśli nie, możesz użyć Provider.of. Jeśli chcielibyśmy odświeżyć stronę po powrocie z drugiego ekranu, możemy zrobić coś takiego:

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

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

inmyth
źródło
0

U mnie pracował:

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

W tym przypadku nie ma znaczenia, w jaki sposób wyskakujesz (przyciskiem w interfejsie użytkownika lub „wstecz” w systemie Android) aktualizacja zostanie wykonana.

Logic2Paradigm
źródło