web-dev-qa-db-ja.com

フラッターのあるネストルート

次の問題に対する優れたアーキテクチャソリューションを見つけようとしています。次の First level ルートはlayoutsとも呼ばれます。

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

ユーザーは、認証状態、アクションなどに応じて、これらのそれぞれにルーティングされます。この段階は正しく取得されました。

たとえば、pagesと呼ばれる Secondary level ルートを使用する場合に問題が発生します。

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

表示するレイアウトとページに効率的にルーティングできるように、これらを最適に定義/整理するにはどうすればよいですか? 1つのレイアウト内でページをルーティングする場合、レイアウトは変更されませんが、ルートに基づいて内部で変化するコンテンツ(ページ)をアニメーション化することに注意してください。

これまでに以下を達成しました

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の下にいる限り、青のビット(レイアウト)は変化しませんが、たとえば/dashboard/homeから/dashboard/statsに移動すると、赤のビット(ページ)はフェードアウトします。新しいコンテンツでフェードインします。 /dashboard/homeから離れて/onboarding/homeと言うと、現在アクティブなページと共に赤いビット(レイアウト)が消え、オンボーディングの新しいレイアウトが表示され、ストーリーが続きます。

enter image description here

[〜#〜] edit [〜#〜] 私は、以下に概説するアプローチで少し進歩しました。基本的に、runApp内のレイアウトを決定し、新しいWidgetsAppおよび各レイアウト内のルート。動作しているようですが、「サインアップ」をクリックすると正しいページにリダイレクトされますが、その下に古いページも表示されます。

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

「ナビゲータ」をネストすることは技術的には可能ですが、ここではお勧めしません(Heroアニメーションが壊れるので)

onGenerateRouteを使用してネストされた「ルート」を構築できます。ルート「/ dashboard/profile」の場合、ツリー_WidgetApp > Dashboard > Profile_を構築します。私はあなたが達成しようとしているものだと思います。

高階関数と組み合わせて、onGenerateRouteを作成するものを作成できます。

コードフローの手がかりを提供するために:NestedRouteはレイアウトの正確なビルドを無視し、builderメソッド(egbuilder: (child) => new Dashboard(child: child),)。 buildRouteメソッドを呼び出すとき、このページのインスタンスに対してPageRouteBuilderを生成しますが、Widgetsの作成を__build_に管理させます。 __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コンポーネントはルーティングシステムと密結合されません。ただし、ルートはネストされています。あなたのルートをあちこちに送るよりも読みやすいです。そして、新しいものを簡単に追加できます。

9
Rémi Rousselet

標準的な Navigator を入れ子にしたまま、追加のトリックなしで使用できます。

enter image description here

必要なのは、 グローバルキー を割り当て、必要なパラメーターを指定することです。そしてもちろん、Android戻るボタン 動作 に注意する必要があります。

知っておく必要があるのは、このナビゲーターのコンテキストがグローバルではないということだけです。それはそれを扱う上でいくつかの特定のポイントにつながります。

次の例はもう少し複雑ですが、ナビゲータウィジェットの外側と内側からネストされたルートを設定する方法を確認できます。例では、setStateinitRouteによって新しいルートを設定するために、ルートページで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」の2つの部分にルートを分離し、このデータを個別に処理する必要があります。

4

構築しようとしているパターンは、たとえ合理的であっても、Flutterを使用してそのままでは表現できないようです。

[〜#〜] edit [〜#〜]:達成したい動作には、onGenerateRouteの使用が必要ですが、まだ(Jan'18)適切に文書化されています( doc )。例については、@ Darkyの回答を参照してください。彼はNestedRouteBuilderNestedRouteの実装を提案し、ギャップを埋めました。

MaterialAppからプレーンナビゲーターを使用して、ルートとページナビゲーション( doc に準拠)には、達成したいことを否定する2つの主な特性があります(少なくとも直接)。 一方ではNavigatorstackとして動作するため、ルートの1つをプッシュおよびポップする次になど、他のルートはfull screenまたはmodal-それらは部分的に画面を占有しますが、それらは下のウィジェットとの相互作用を禁止します。もっと明示的に言えば、あなたのパラダイムはスタック内のdifferentレベルのページとの同時対話を必要とするようです-これはできません。

さらに、パスパラダイムは階層(一般的なフレーム→特定のサブページ)だけでなく、最初のインスタンスのナビゲーターのスタックの表現でもあるように感じます。私自身はだまされましたが、読むと明らかになります this

String initialRoute

final

表示する最初のルートの名前。

デフォルトでは、これはDart:ui.Window.defaultRouteNameに従います。

この文字列に/文字が含まれている場合、文字列はそれらの文字で分割され、文字列の先頭から各文字までの部分文字列は、プッシュへのルートとして使用されます。

たとえば、ルート/ stocks/HOOLIがinitialRouteとして使用された場合、ナビゲーターは起動時に次のルートをプッシュします:/、/ stocks、/ stocks/HOOLI。これにより、アプリケーションで予測可能なルート履歴を維持しながら、ディープリンクが可能になります。

可能な回避策は、次に示すように、パス名を活用して子ウィジェットをインスタンス化し、表示する内容を知るために状態変数を保持することです。

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

ただし、これには、下位レベルにstack memoryがありません。サブルートウィジェットのシーケンスを処理する場合-サブルートコンテナをWillPopScopeでラップし、ユーザーがbackボタンを押したときに何をするかを定義して、スタック内のサブルートのシーケンス。しかし、私はそのようなことを提案する気はありません。

私の最後の提案は、「レベル」なしでプレーンルートを実装し、カスタムトランジションを管理して「外部」レイアウトの変更を隠し、データをページに渡すか、アプリの状態を提供する適切なクラスを維持することです。

PS: Hero アニメーションもチェックしてください。ビュー間で探している連続性を提供できます。

3
Fabio Veronese