web-dev-qa-db-fra.com

Itinéraires de nidification avec flottement

J'essaie de trouver une bonne solution architecturale pour le problème suivant: J'ai des itinéraires de premier niveau qui peuvent également être appelés dispositions:

/onboarding/* -> Shows onboarding layout
/dashboard/* -> Shows dashboard layout
/overlay/* -> shows slide up overlay layout
/modal/* -> shows modal layout

L'utilisateur est acheminé vers chacun d'eux en fonction de son état d'authentification, de ses actions, etc. J'ai obtenu cette étape correctement.

Des problèmes surviennent lorsque je veux utiliser des routes de niveau secondaire qui peuvent être appelées pages, par exemple

/onboarding/signin -> Shows onboarding layout, that displays signin route
/onboarding/plan -> Shows onboarding layout, that displays plan options
/modal/plan-info -> Shows modal layout, over previous page (/onboarding/plan) and displays plan-information page.

Comment puis-je définir/organiser au mieux ceux-ci de manière à ce que je puisse efficacement router vers les mises en page et les pages qu'ils affichent? Notez que chaque fois que je route des pages à l'intérieur d'une mise en page, la mise en page ne change pas, mais je veux animer du contenu (pages) qui change à l'intérieur en fonction de la route.

Jusqu'à présent, j'ai réussi à suivre

import "package:flutter/widgets.Dart";
import "package:skimitar/layouts/Onboarding.Dart";
import "package:skimitar/layouts/Dashboard.Dart";

Route generate(RouteSettings settings) {
  Route page;
  switch (settings.name) {
    case "/onboarding":
      page = new PageRouteBuilder(pageBuilder: (BuildContext context,
          Animation<double> animation, Animation<double> secondaryAnimation) {
        return new Onboarding();
      });
      break;
      case "/dashboard":
      page = new PageRouteBuilder(pageBuilder: (BuildContext context,
          Animation<double> animation, Animation<double> secondaryAnimation) {
        return new Dashboard();
      });
      break;
  }
  return page;
}

/* Main */
void main() {
  runApp(new WidgetsApp(
      onGenerateRoute: generate, color: const Color(0xFFFFFFFFF)));
}

Cela permet d'accéder aux dispositions d'embarquement et de tableau de bord (pour le moment, de simples conteneurs enveloppent le texte). Je crois également que je peux utiliser PageRouteBuilder ce dernier pour animer les transitions entre les routes? Maintenant, je dois comprendre comment avoir quelque chose comme routeur secondaire imbriqué à l'intérieur sur l'embarquement et le tableau de bord.

Ci-dessous est une représentation visuelle de ce que je veux réaliser, je dois être en mesure de router avec succès des bits bleus et rouges. Dans cet exemple, tant que nous sommes sous /dashboard le bit bleu (mise en page) ne change pas, mais alors que nous naviguons à partir de dire /dashboard/home à /dashboard/stats le bit rouge (page) doit disparaître et apparaître avec le nouveau contenu. Si nous nous éloignons de /dashboard/home dire /onboarding/home, le bit rouge (mise en page) devrait s'estomper, ainsi que sa page actuellement active et afficher une nouvelle mise en page pour l'intégration et l'histoire continue.

enter image description here

[~ # ~] modifier [~ # ~] J'ai fait un peu de progrès avec l'approche décrite ci-dessous, essentiellement je vais déterminer la disposition à l'intérieur de mon runApp et déclarera de nouveaux WidgetsApp et des routes à l'intérieur de chacune des dispositions. Cela semble fonctionner, mais il y a un problème, lorsque je clique sur "S'inscrire", je suis redirigé vers la page correcte, mais je peux également voir l'ancienne page en dessous.

main.Dart

import "package:flutter/widgets.Dart";
import "package:myProject/containers/layouts/Onboarding.Dart";

/* Main */
void main() {
  runApp(new Onboarding());
}

Onboarding.Dart

import "package:flutter/widgets.Dart";
import "package:myProject/containers/pages/SignIn.Dart";
import "package:myProject/containers/pages/SignUp.Dart";
import "package:myProject/services/helpers.Dart";

/* Onboarding router */
Route onboardingRouter(RouteSettings settings) {
  Route page;
  switch (settings.name) {
    case "/":
      page = buildOnboardingRoute(new SignIn());
      break;
    case "/sign-up":
      page = buildOnboardingRoute(new SignUp());
      break;
    default:
      page = buildOnboardingRoute(new SignIn());
  }
  return page;
}

class Onboarding extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Container(
      decoration: new BoxDecoration(
          color: const Color(0xFF000000),
          image: new DecorationImage(
              image: new AssetImage("assets/images/background-fire.jpg"),
              fit: BoxFit.cover)),
      child: new WidgetsApp(
          onGenerateRoute: onboardingRouter, color: const Color(0xFF000000)),
    );
  }
}

SignUp.Dart

import "package:flutter/widgets.Dart";

class SignUp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Center(
        child: new Text("Sign Up",
            style: new TextStyle(color: const Color(0xFFFFFFFF))));
  }
}

helpers.Dart

import "package:flutter/widgets.Dart";

Route buildOnboardingRoute(Widget page) {
  return new PageRouteBuilder(
      opaque: true,
      pageBuilder: (BuildContext context, _, __) {
        return page;
      });
}
20
Ilja

Bien qu'il soit techniquement possible d'imbriquer "Navigator", cela n'est pas recommandé ici (car il casse Hero animation)

Vous pouvez utiliser onGenerateRoute pour construire des 'routes' imbriquées, dans le cas d'une route '/ dashboard/profile', construire une arborescence WidgetApp > Dashboard > Profile. Je suppose que c'est ce que vous essayez de réaliser.

Combiné avec une fonction d'ordre supérieur, vous pouvez avoir quelque chose qui crée onGenerateRoute pour vous.

Pour fournir un indice du flux de code: le NestedRoute néglige la construction exacte de la mise en page, le laissant dans la méthode builder (par exemplebuilder: (child) => new Dashboard(child: child),). Lors de l'appel de la méthode buildRoute, nous générerons un PageRouteBuilder pour l'instance même de cette page, mais en laissant _build Gérer la création du Widgets. Dans _build, Nous utilisons le builder tel quel - ou le laissons gonfler la sous-route, en rappelant la sous-route demandée, en appelant sa propre _build. Une fois cela fait, nous utiliserons la sous-route construite comme argument de notre constructeur. Pour faire court, vous plongez récursivement dans d'autres niveaux de chemin pour créer le dernier niveau de l'itinéraire, puis le laissez s'élever de la récursivité et utilisez le résultat comme argument pour le niveau extérieur, etc.

BuildNestedRoutes fait le sale boulot pour vous et analyse les listes de NestedRoutes pour construire le RouteSettings nécessaire.

Ainsi, à partir de l'exemple ci-dessous

Exemple :

@override
Widget build(BuildContext context) {
  return new MaterialApp(
    initialRoute: '/foo/bar',
    home: const FooBar(),
    onGenerateRoute: buildNestedRoutes(
      [
        new NestedRoute(
          name: 'foo',
          builder: (child) => new Center(child: child),
          subRoutes: [
            new NestedRoute(
              name: 'bar',
              builder: (_) => const Text('bar'),
            ),
            new NestedRoute(
              name: 'baz',
              builder: (_) => const Text('baz'),
            )
          ],
        ),
      ],
    ),
  );
}

Ici, vous avez simplement défini vos routes imbriquées (nom + composant associé). Et la méthode NestedRoute class + buildNestedRoutes est définie de cette façon:

typedef Widget NestedRouteBuilder(Widget child);

@immutable
class NestedRoute {
  final String name;
  final List<NestedRoute> subRoutes;
  final NestedRouteBuilder builder;

  const NestedRoute({@required this.name, this.subRoutes, @required this.builder});

  Route buildRoute(List<String> paths, int index) {
    return new PageRouteBuilder<dynamic>(
      pageBuilder: (_, __, ___) => _build(paths, index),
    );
  }

  Widget _build(List<String> paths, int index) {
    if (index > paths.length) {
      return builder(null);
    }
    final route = subRoutes?.firstWhere((route) => route.name == paths[index], orElse: () => null);
    return builder(route?._build(paths, index + 1));
  }
}

RouteFactory buildNestedRoutes(List<NestedRoute> routes) {
  return (RouteSettings settings) {
    final paths = settings.name.split('/');
    if (paths.length <= 1) {
      return null;
    }
    final rootRoute = routes.firstWhere((route) => route.name == paths[1]);
    return rootRoute.buildRoute(paths, 2);
  };
}

De cette façon, vos composants Foo et Bar ne seront pas étroitement couplés à votre système de routage; mais ont toujours des itinéraires imbriqués. C'est plus lisible que d'avoir vos itinéraires distribués partout. Et vous en ajouterez facilement un nouveau.

9
Rémi Rousselet

Vous pouvez utiliser le standard Navigator comme imbriqué, sans astuces supplémentaires.

enter image description here

Tout ce dont vous avez besoin, c'est d'affecter un clé globale et de spécifier les paramètres nécessaires. Et bien sûr, vous devez vous soucier de Android bouton de retour comportement .

La seule chose que vous devez savoir est que le contexte de ce navigateur ne sera pas global. Cela conduira à certains points spécifiques dans le travail avec elle.

L'exemple suivant est un peu plus compliqué, mais il vous permet de voir comment vous pouvez définir les itinéraires imbriqués de l'extérieur et de l'intérieur pour le widget navigateur. Par exemple, nous appelons setState dans la page racine pour définir la nouvelle route par initRoute de NestedNavigator.

  import 'package:flutter/material.Dart';

  void main() => runApp(App());

  class App extends StatelessWidget {
    // This widget is the root of your application.
    @override
    Widget build(BuildContext context) {
      return MaterialApp(
        title: 'Nested Routing Demo',
        home: HomePage(),
      );
    }
  }

  class HomePage extends StatefulWidget {
    @override
    _HomeState createState() => _HomeState();
  }

  class _HomeState extends State<HomePage> {
    final GlobalKey<NavigatorState> navigationKey = GlobalKey<NavigatorState>();

    @override
    Widget build(BuildContext context) {
      return Scaffold(
        appBar: AppBar(
          title: Text('Root App Bar'),
        ),
        body: Column(
          children: <Widget>[
            Container(
              height: 72,
              color: Colors.cyanAccent,
              padding: EdgeInsets.all(18),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.center,
                children: <Widget>[
                  Text('Change Inner Route: '),
                  RaisedButton(
                    onPressed: () {
                      while (navigationKey.currentState.canPop())
                        navigationKey.currentState.pop();
                    },
                    child: Text('to Root'),
                  ),
                ],
              ),
            ),
            Expanded(
              child: NestedNavigator(
                navigationKey: navigationKey,
                initialRoute: '/',
                routes: {
                  // default rout as '/' is necessary!
                  '/': (context) => PageOne(),
                  '/two': (context) => PageTwo(),
                  '/three': (context) => PageThree(),
                },
              ),
            ),
          ],
        ),
      );
    }
  }

  class NestedNavigator extends StatelessWidget {
    final GlobalKey<NavigatorState> navigationKey;
    final String initialRoute;
    final Map<String, WidgetBuilder> routes;

    NestedNavigator({
      @required this.navigationKey,
      @required this.initialRoute,
      @required this.routes,
    });

    @override
    Widget build(BuildContext context) {
      return WillPopScope(
        child: Navigator(
          key: navigationKey,
          initialRoute: initialRoute,
          onGenerateRoute: (RouteSettings routeSettings) {
            WidgetBuilder builder = routes[routeSettings.name];
            if (routeSettings.isInitialRoute) {
              return PageRouteBuilder(
                pageBuilder: (context, __, ___) => builder(context),
                settings: routeSettings,
              );
            } else {
              return MaterialPageRoute(
                builder: builder,
                settings: routeSettings,
              );
            }
          },
        ),
        onWillPop: () {
          if(navigationKey.currentState.canPop()) {
            navigationKey.currentState.pop();
            return Future<bool>.value(false);
          }
          return Future<bool>.value(true);
        },
      );
    }
  }

  class PageOne extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text('Page One'),
              RaisedButton(
                onPressed: () {
                  Navigator.of(context).pushNamed('/two');
                },
                child: Text('to Page Two'),
              ),
            ],
          ),
        ),
      );
    }
  }

  class PageTwo extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text('Page Two'),
              RaisedButton(
                onPressed: () {
                  Navigator.of(context).pushNamed('/three');
                },
                child: Text('go to next'),
              ),
              RaisedButton(
                onPressed: () {
                  Navigator.of(context).pop();
                },
                child: Text('go to back'),
              ),
            ],
          ),
        ),
      );
    }
  }

  class PageThree extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
      return Scaffold(
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Text('Page Three'),
              RaisedButton(
                onPressed: () {
                  Navigator.of(context).pop();
                },
                child: Text('go to back'),
              ),
            ],
          ),
        ),
      );
    }
  }

Vous pouvez trouver des informations supplémentaires dans le article suivant .

Malheureusement, vous ne pouvez pas naviguer vers le même widget racine sans pile de navigation , lorsque vous modifiez uniquement l'enfant. Donc, pour éviter la navigation du widget racine ( la duplication du widget racine ), vous devez créer une méthode de navigation personnalisée, par exemple basée sur InheritedWidget. Dans lequel vous vérifierez le nouvel itinéraire racine et s'il n'a pas changé pour appeler uniquement le navigateur enfant (imbriqué).

Vous devez donc séparer votre itinéraire en deux parties: '/ onboarding' pour le navigateur racine et '/ plan' pour le navigateur imbriqué et traiter ces données séparément.

4
Yuriy Luchaninov

Le motif que vous essayez de construire, même s'il est raisonnable, semble qu'il ne peut pas être représenté hors de la boîte avec Flutter.

[~ # ~] edit [~ # ~] : Le comportement que vous souhaitez obtenir nécessite l'utilisation de onGenerateRoute, mais pas encore (Jan'18) correctement documenté ( doc ). Voir la réponse @Darky pour avoir un exemple. Il propose des implémentations NestedRouteBuilder et NestedRoute, comblant le vide.

En utilisant plain Navigator à partir d'un MaterialApp, les itinéraires et la navigation dans les pages (selon doc ) ont deux caractéristiques principales qui nient ce que vous voulez réaliser (au moins directement). D'une part, le Navigator se comporte comme une pile, poussant et sautant ainsi les routes les unes sur les autres et ainsi de suite, = de l'autre les itinéraires sont soit plein écran ou modal - ce qui signifie qu'ils occupent partiellement l'écran, mais ils inhibe l'interaction avec les widgets en dessous. Plus explicite, votre paradigme semble exiger l'interaction simultanée avec des pages à différents niveaux dans la pile - ce qui ne peut pas être fait de cette façon.

De plus, il semble que le paradigme du chemin ne soit pas seulement une hiérarchie - cadre général → sous-page spécifique - mais en premier lieu une représentation de la pile dans le navigateur . J'ai moi-même été trompé, mais cela devient clair: this :

String initialRoute

final

Nom du premier itinéraire à afficher.

Par défaut, cela revient à Dart: ui.Window.defaultRouteName.

Si cette chaîne contient des caractères /, la chaîne est divisée sur ces caractères et les sous-chaînes depuis le début de la chaîne jusqu'à chacun de ces caractères sont, à leur tour, utilisées comme routes vers Push.

Par exemple, si la route/stocks/HOOLI était utilisée comme route initiale, le navigateur pousserait les routes suivantes au démarrage: /,/stocks,/stocks/HOOLI. Cela permet un lien profond tout en permettant à l'application de conserver un historique d'itinéraire prévisible.

Une solution de contournement possible, comme suit, consiste à exploiter le chemin pour instancier les widgets enfants, en conservant une variable d'état pour savoir ce qu'il faut afficher:

import 'package:flutter/material.Dart';

void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new ActionPage(title: 'Flutter Demo Home Page'),
      routes: <String, WidgetBuilder>{
        '/action/plus': (BuildContext context) => new ActionPage(sub: 'plus'),
        '/action/minus': (BuildContext context) => new ActionPage(sub: 'minus'),
      },
    );
  }
}

class ActionPage extends StatefulWidget {
  ActionPage({Key key, this.title, this.sub = 'plus'}) : super(key: key);

  final String title, sub;

  int counter;

  final Map<String, dynamic> subroutes = {
    'plus': (BuildContext context, int count, dynamic setCount) =>
        new PlusSubPage(count, setCount),
    'minus': (BuildContext context, int count, dynamic setCount) =>
        new MinusSubPage(count, setCount),
  };

  @override
  ActionPageState createState() => new ActionPageState();
}

class ActionPageState extends State<ActionPage> {
  int _main_counter = 0;

  String subPageState;

  @override
  void initState() {
    super.initState();
    subPageState = widget.sub;
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        appBar: new AppBar(
          title: new Text('Testing subpages'),
          actions: <Widget>[
            new FlatButton(
                child: new Text('+1'),
                onPressed: () {
                  if (subPageState != 'plus') {
                    setState(() => subPageState = 'plus');
                    setState(() => null);
                  }
                }),
            new FlatButton(
                child: new Text('-1'),
                onPressed: () {
                  if (subPageState != 'minus') {
                    setState(() => subPageState = 'minus');
                    setState(() => null);
                  }
                }),
          ],
        ),
        body: widget.subroutes[subPageState](context, _main_counter, (count) {
          _main_counter = count;
        }));
  }
}

class PlusSubPage extends StatefulWidget {
  PlusSubPage(this.counter, this.setCount);
  final setCount;
  final int counter;
  @override
  _PlusSubPageState createState() => new _PlusSubPageState();
}

class _PlusSubPageState extends State<PlusSubPage> {
  int _counter = 0;

  @override
  void initState() {
    super.initState();
    _counter = widget.counter;
  }

  void _incrementCounter() {
    setState(() {
      _counter++;
      widget.setCount(_counter);
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Center(
      child: new Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          new IconButton(
            icon: const Icon(Icons.add),
            onPressed: _incrementCounter,
          ),
          new Text(
            'You have pushed the button this many times:',
          ),
          new Text(
            '$_counter',
            style: Theme.of(context).textTheme.display1,
          ),
        ],
      ),
    );
  }
}

class MinusSubPage extends StatefulWidget {
  MinusSubPage(this.counter, this.setCount);
  final setCount;
  final int counter;
  @override
  _MinusSubPageState createState() => new _MinusSubPageState();
}

class _MinusSubPageState extends State<MinusSubPage> {
  int _counter = 0;

  @override
  void initState() {
    super.initState();
    _counter = widget.counter;
  }

  void _decrementCounter() {
    setState(() {
      _counter--;
      widget.setCount(_counter);
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Center(
      child: new Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          new IconButton(
            icon: const Icon(Icons.remove),
            onPressed: _decrementCounter,
          ),
          new Text(
            'You have pushed the button this many times:',
          ),
          new Text(
            '$_counter',
            style: Theme.of(context).textTheme.display1,
          ),
        ],
      ),
    );
  }
}

Cependant, cela n'a pas de mémoire de pile à un niveau inférieur. Dans le cas où vous souhaitez gérer la séquence de widget de sous-routes - vous pouvez envelopper le conteneur de sous-routes dans un WillPopScope, en définissant là ce qu'il est censé faire lorsque l'utilisateur appuie sur le bouton back et en stockant le séquence des sous-routes dans une pile. Cependant, je n'ai pas envie de suggérer une telle chose.

Ma dernière suggestion est d'implémenter des routes simples - sans "niveaux" -, de gérer des transitions personnalisées pour masquer le changement de disposition "externe" et de passer les données à travers les pages ou de conserver une classe appropriée en vous fournissant l'état de l'application.

PS: vérifiez également les animations Hero , elles peuvent vous fournir la continuité que vous recherchez entre les vues.

3
Fabio Veronese