Маршруты вложения с флаттером

Я пытаюсь найти хорошее архитектурное решение для следующей проблемы: у меня есть следующие маршруты первого уровня, которые также можно назвать макетами:

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

Пользователь перенаправляется каждому из них в зависимости от своего состояния, действий и т.д. Я правильно понял этот этап.

Проблемы возникают, когда я хочу использовать маршруты Вторичный уровень, которые можно назвать страницами, например

/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.

Как я могу лучше всего определить/организовать их таким образом, чтобы я мог эффективно маршрутизировать макеты и страницы, которые они отображают? Обратите внимание, что всякий раз, когда я маршрутизирую страницы внутри одного макета, макет не меняется, но я хочу анимировать содержимое (страницы), которые меняются внутри него на основе маршрута.

До сих пор я добился следующих результатов:

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

Это маршрут к макетам на борту и на панели управления (прямо сейчас просто переносится текст контейнера). Я также считаю, что я могу использовать PageRouteBuilder последний для анимации переходов между маршрутами? Теперь мне нужно выяснить, как сделать что-то вроде вложенного вторичного маршрутизатора внутри на борту и на панели управления.

Ниже представлено несколько визуальное представление того, чего я хочу достичь, мне нужно иметь возможность успешно маршрутизировать синие и красные биты. В этом примере, пока мы находимся под /dashboard синий бит (макет) не изменяется, но по мере перехода от say /dashboard/home к /dashboard/stats красный бит (страница) должен исчезать и исчезать с новым контентом, Если мы удалимся от /dashboard/home, чтобы сказать /onboarding/home, красный бит (макет) должен исчезнуть вместе с его активной активной страницей и показать новый макет для навигации, и история продолжается.

введите описание изображения здесь

EDIT Я немного поработал с подходом, описанным ниже, по сути, я определю макет внутри своего runApp и объявит новый WidgetsApp и проложит внутри каждого из макетов. Кажется, что это работает, но есть проблема. Когда я нажимаю "SignUp", меня перенаправляют на правильную страницу, но я также могу увидеть старую страницу ниже.

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

Ответ 1

Хотя технически возможно вложить "Навигатор", здесь это не рекомендуется (поскольку оно нарушает анимацию героев)

Вы можете использовать onGenerateRoute для создания вложенных "маршрутов", в случае маршрута "/dashboard/profile", построить Tree WidgetApp > Dashboard > Profile. Я полагаю, что вы пытаетесь достичь.

В сочетании с функцией более высокого порядка у вас может быть что-то, что создает onGenerateRoute для вас.

Чтобы получить представление о потоке кода: NestedRoute игнорирует точную сборку макета, пропуская его в методе builder (например, builder: (child) => new Dashboard(child: child),). При вызове метода buildRoute мы PageRouteBuilder для самого экземпляра этой страницы, но позволяя _build управлять созданием Widgets. В _build мы либо используем builder как есть - либо позволяем ему раздувать подчиненный маршрут, вызывая запрашиваемый подчиненный маршрут, вызывая его собственный _build. После этого мы будем использовать встроенный подчиненный маршрут в качестве аргумента нашего сборщика. Короче говоря, вы рекурсивно погружаетесь в дальнейшие уровни пути, чтобы построить последний уровень маршрута, затем позволяете ему подняться из рекурсии и используете результат в качестве аргумента для внешнего уровня и так далее.

BuildNestedRoutes выполняет грязную работу за вас и анализирует списки NestedRoutes для создания необходимых RouteSettings.

Итак, из приведенного ниже примера

Пример:

@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'),
            )
          ],
        ),
      ],
    ),
  );
}

Здесь вы просто определили свои вложенные маршруты (имя + связанный компонент). И класс NestedRoute + метод buildNestedRoutes определяются следующим образом:

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

Таким образом, ваши компоненты Foo и Bar не будут тесно связаны с вашей системой маршрутизации; но все еще есть вложенные маршруты. Это более читабельно, чем отправка ваших маршрутов повсюду. И вы легко добавите новый.

Ответ 2

Шаблон, который вы пытаетесь построить, даже если это разумно, кажется, что он не может быть представлен из коробки с Flutter.

РЕДАКТИРОВАТЬ. Поведение, которое вы хотите достичь, требует использования onGenerateRoute, однако еще не написано (Jan'18) (doc). См. Ответ @Darky, чтобы привести пример. Он предлагает реализации NestedRouteBuilder и NestedRoute, заполняя пробел.

Использование простого навигатора из MaterialApp, навигации по маршрутам и страницам (в соответствии с doc) есть две основные характеристики, которые отрицают то, что вы хотите для достижения (по крайней мере, непосредственно). С одной стороны, Navigator ведет себя как стек, тем самым выдвигая и выскакивая маршруты один поверх следующего и т.д., На других маршрутах либо полноэкранный, либо модальный - это означает, что они занимают экран частично, но они запретить взаимодействие с виджетами ниже. Являясь более явным, ваша парадигма, похоже, требует одновременного взаимодействия со страницами на уровне разных в стеке - это невозможно сделать таким образом.

Кроме того, похоже, что парадигма пути - это не только иерархия - общий кадр → конкретная подстраница, но в первом случае представление стека в навигаторе. Я сам обманул, но становится ясно, что читаем this:

String initialRoute

final

Имя первого отображаемого маршрута.

По умолчанию это отбрасывает дротик: ui.Window.defaultRouteName.

Если эта строка содержит любые/символы, то строка разделяется на те символы и подстроки от начала строки до каждый из них, в свою очередь, используется в качестве маршрутов для нажатия.

Например, если маршрут/акции/HOOLI использовался в качестве начальногоRoute, то Navigator будет запускать следующие маршруты при запуске:/, /акции, акции/ХОЛОЛИ. Это обеспечивает глубокое связывание, позволяя приложение для поддержания прогнозируемой истории маршрута.

Возможное обходное решение, как следует, заключается в том, чтобы использовать путь для создания экземпляров дочерних виджетов, сохраняя переменную состояния, чтобы знать, что показывать:

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,
          ),
        ],
      ),
    );
  }
}

Это, однако, не имеет памяти стека на более низком уровне. Если вы хотите обработать последовательность виджета подпрограмм, вы можете обернуть контейнер подпрограммы в WillPopScope, указав там, что он должен делать, когда пользователь нажимает кнопку back и сохраняет последовательность подпрограмм в стек. Однако я не хочу предлагать такую ​​вещь.

Мое последнее предложение - реализовать простые маршруты - без "уровней" - управлять настраиваемыми переходами, чтобы скрыть изменение "внешнего" макета и передать данные через страницы или сохранить в соответствующем классе, предоставляя вам состояние приложения.

PS: проверьте также Hero анимации, они могут обеспечить вам непрерывность, которую вы ищете между представлениями.

Ответ 3

Вы можете использовать стандартный навигатор как вложенный, без каких-либо дополнительных хитростей.

enter image description here

Все, что вам нужно, это назначить глобальный ключ и указать необходимые параметры. И, конечно же, вам нужно заботиться о поведении кнопки "назад" на Android.

Единственное, что вам нужно знать, это то, что контекст этого навигатора не будет глобальным. Это приведет к определенным моментам в работе с ним.

Следующий пример немного сложнее, но он позволяет вам увидеть, как вы можете установить вложенные маршруты снаружи и внутри для виджета навигатора. В примере мы вызываем setState на корневой странице для установки нового маршрута с помощью initRoute из 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'),
              ),
            ],
          ),
        ),
      );
    }
  }

Вы можете найти дополнительную информацию в следующей статье.

К сожалению, вы не можете перейти к тому же корневому виджету без стека навигации, когда вы изменяете только дочерний виджет. Поэтому, чтобы избежать навигации по корневому виджету (дублирование корневого виджета), вам необходимо создать собственный метод навигации, например, на основе InheritedWidget. В котором вы будете проверять новый корневой маршрут и, если он не изменен, вызывать только дочерний (вложенный) навигатор.

Поэтому вам нужно разделить маршрут на две части: "/onboarding" для корневого навигатора и "/plan" для вложенного навигатора и обработать эти данные отдельно.