次の問題に対する優れたアーキテクチャソリューションを見つけようとしています。次の 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
と言うと、現在アクティブなページと共に赤いビット(レイアウト)が消え、オンボーディングの新しいレイアウトが表示され、ストーリーが続きます。
[〜#〜] 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;
});
}
「ナビゲータ」をネストすることは技術的には可能ですが、ここではお勧めしません(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
コンポーネントはルーティングシステムと密結合されません。ただし、ルートはネストされています。あなたのルートをあちこちに送るよりも読みやすいです。そして、新しいものを簡単に追加できます。
標準的な Navigator を入れ子にしたまま、追加のトリックなしで使用できます。
必要なのは、 グローバルキー を割り当て、必要なパラメーターを指定することです。そしてもちろん、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」の2つの部分にルートを分離し、このデータを個別に処理する必要があります。
構築しようとしているパターンは、たとえ合理的であっても、Flutterを使用してそのままでは表現できないようです。
[〜#〜] edit [〜#〜]:達成したい動作には、onGenerateRouteの使用が必要ですが、まだ(Jan'18)適切に文書化されています( doc )。例については、@ Darkyの回答を参照してください。彼はNestedRouteBuilder
とNestedRoute
の実装を提案し、ギャップを埋めました。
MaterialAppからプレーンナビゲーターを使用して、ルートとページナビゲーション( doc に準拠)には、達成したいことを否定する2つの主な特性があります(少なくとも直接)。 一方では、Navigator
はstackとして動作するため、ルートの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 アニメーションもチェックしてください。ビュー間で探している連続性を提供できます。