TOP(About this memo)) > 一覧(Flutter) > ルーティング
Flutter provides a complete system for navigating between screens and handling deep links. Small applications without complex deep linking can use Navigator, while apps with specific deep linking and navigation requirements should also use the Router to correctly handle deep links on Android and iOS, and to stay in sync with the address bar when the app is running on the web.
Navigator自体はStatefulWidgetである。
ナビゲーションスタックで遷移を管理する。
スタック情報はNavigator.pagesとなる。
class Navigator extends StatefulWidget {
// ...
final List<Page<dynamic>> pages;
// ...
}
Page
PageRoute
Navigator.of(context) はBuildContextからNavigatorStateを取得する
サンプルコード
main() => runApp(MaterialApp(
home: Navigator(
pages: const [
MaterialPage(child: MyWidget("page C")),
MaterialPage(child: MyWidget("page B")),
MaterialPage(child: MyWidget("page A")),
MaterialPage(child: Top())
],
// pages利用の際は、onPopPageを設定する必要がある。設定しない場合はエラーとなる。
// また、onPopPageのAPIドキュメントに記載の通りonPopPageはroute.didPopを実行して結果をreturnする責任がある。
// したがって「onPopPage: (route, result) => true,」のように実装するとassertエラーとなってしまう。
onPopPage: (route, result) => route.didPop(result),
)));
class Top extends StatelessWidget {
const Top({super.key});
@override
Widget build(BuildContext context) {
return MyWidget("top page",
child: Column(
children: [
TextButton(
onPressed: () {
// MaterialPageRouteとCupertinoPageRouteで遷移のアニメーションが異なることがわかる。
//Navigator.of(context).push(MaterialPageRoute(
// builder: (_) => const MyWidget("pushed page")));
Navigator.of(context).push(CupertinoPageRoute(
builder: (_) => const MyWidget("pushed page")));
},
child: const Text("push page"),
),
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text("pop page"))
],
));
}
}
class MyWidget extends StatelessWidget {
const MyWidget(this.pageName, {this.child, super.key});
final String pageName;
final Widget? child;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(pageName),
),
body: child,
);
}
}
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
main() => runApp(const MaterialApp(home: MyWidget()));
class MyWidget extends StatefulWidget {
const MyWidget({super.key});
@override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
List<Page> pages = const [
MaterialPage(child: Content("page C")),
MaterialPage(child: Content("page B")),
MaterialPage(child: Content("page A")),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: Navigator(
pages: pages,
onPopPage: (route, result) => route.didPop(result),
),
floatingActionButton: FloatingActionButton(onPressed: () {
// CupertinoPage, MaterialPageのそれぞれで異なったアニメーションが確認できる
pages = [const CupertinoPage(child: Content("added page"))];
//pages = [const MaterialPage(child: Content("added page"))];
setState((){});
}),
);
}
}
class Content extends StatelessWidget {
const Content(this.pageName, {this.child, super.key});
final String pageName;
final Widget? child;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(pageName),
),
body: child,
);
}
}
// 公式サンプル
MaterialApp(
routes: {
'/': (context) => HomeScreen(),
'/details': (context) => DetailScreen(),
},
)
onPressed: () {
Navigator.pushNamed(
context,
'/details',
arguments: 3, // Object?型
);
},
MaterialApp.router(
// ...
routerConfig: RouterConfig(//...),
);
MaterialApp.router(
// ...
routerConfig: GoRouter(//...),
);
Note: The FlutterDeepLinkingEnabled property opts into Flutter’s default deeplink handler. If you are using the third-party plugins, such as uni_links, setting this property will break these plugins. Skip this step if you prefer to use third-party plugins.
<key>FlutterDeepLinkingEnabled</key>
<true/>
class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
// ...
bool get _usesRouterWithDelegates => widget.routerDelegate != null;
bool get _usesRouterWithConfig => widget.routerConfig != null;
bool get _usesNavigator => widget.home != null
|| (widget.routes?.isNotEmpty ?? false)
|| widget.onGenerateRoute != null
|| widget.onUnknownRoute != null;
// ...
Widget build(BuildContext context) {
Widget? routing;
if (_usesRouterWithDelegates) {
routing = Router<Object>(/* ... */, routerDelegate: widget.routerDelegate!,);
} else if (_usesNavigator) {
routing = FocusScope(
//...
child: Navigator(/* .... */),
);
} else if (_usesRouterWithConfig) {
routing = Router<Object>.withConfig(
//...
config: widget.routerConfig!,
);
}
}
}
class Navigator extends StatefulWidget {
// ...
final List<Page<dynamic>> pages;
// ...
}
class NavigatorState extends State<Navigator> with TickerProviderStateMixin, RestorationMixin {
//...
_History _history = _History();
//...
bool get _usingPagesAPI => widget.pages != const <Page<dynamic>>[];// _usingPagesAPIはassertのみで利用。
//...
void initState() {
//...
_history.addListener(_handleHistoryChanged);
}
//...
// restoreStateはState.initStateの直後に呼ばれる。
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
//...
for (final Page<dynamic> page in widget.pages) {
final _RouteEntry entry = _RouteEntry(
page.createRoute(context),
pageBased: true,
initialState: _RouteLifecycle.add,
);
//...
_history.add(entry);
_history.addAll(_serializableHistory.restoreEntriesForPage(entry, this));
}
}
//...
void _handleHistoryChanged() {
//...
notification.dispatch(context); // NavigationNotificationをdispatch
}
//...
Future<T?> push<T extends Object?>(Route<T> route) {
_pushEntry(_RouteEntry(route, pageBased: false, initialState: _RouteLifecycle.push));
return route.popped;
}
//...
void _pushEntry(_RouteEntry entry) {
//...
_history.add(entry);
_flushHistoryUpdates();
//...
}
void _flushHistoryUpdates({bool rearrangeOverlay = true}) {
//...
int index = _history.length - 1;
_RouteEntry? next;
_RouteEntry? entry = _history[index];
_RouteEntry? previous = index > 0 ? _history[index - 1] : null;
while (index >= 0) {
switch (entry!.currentState) {
//...
}
}
// ...
if (rearrangeOverlay) {
// overlay は _overlayKey.currentStateでOverlayStateを取得。(これはbuildで生成しているOverlayオブジェクトのState)
// _allRouteOverlayEntriesでは、entry.route.overlayEntriesは_historyから取得した
// Iterable<OverlayEntry>であり、これがOverlay._entriesへ追加・setStateされている。
overlay?.rearrange(_allRouteOverlayEntries);
}
// ...
}
Widget build(BuildContext context) {
NotificationListener<NavigationNotification>(
//...
Overlay(
key: _overlayKey,
initialEntries: overlay == null ? _allRouteOverlayEntries.toList(growable: false) : const <OverlayEntry>[],
)
}
}
enum _RouteLifecycle {
staging,
add,
adding,
push,
pushReplace,
pushing,
replace,
idle,
pop,
complete,
remove,
popping,
removing,
dispose,
disposing,
disposed,
}
class _History extends Iterable<_RouteEntry> with ChangeNotifier {
//...
}
class _RouteEntry extends RouteTransitionRecord {
// ...
final Route<dynamic> route;
// ...
_RouteLifecycle currentState;
}
abstract class Route<T> extends _RoutePlaceholder {
//...
NavigatorState? get navigator => _navigator;
NavigatorState? _navigator;
//...
List<OverlayEntry> get overlayEntries => const <OverlayEntry>[];
}
class OverlayEntry implements Listenable {
//...
final WidgetBuilder builder;
//...
}
class OverlayState extends State<Overlay> with TickerProviderStateMixin {
final List<OverlayEntry> _entries = <OverlayEntry>[];
@override
void initState() {
//...
insertAll(widget.initialEntries);
}
//...
void rearrange(Iterable<OverlayEntry> newEntries, { OverlayEntry? below, OverlayEntry? above }) {
// ...
setState(() {
//...
_entries.addAll(newEntriesList);
});
}
Widget build(BuildContext context) {
//...
// 最終的に子孫の _OverlayEntryWidgetState.build()内で OverlayEntry.builder()が実行される。
}
}
WidgetsApp.routerに渡すRouterConfigは4つの抽象で構成される。
class RouterConfig<T> {
const RouterConfig({
this.routeInformationProvider,
this.routeInformationParser,
required this.routerDelegate,
this.backButtonDispatcher,
}) : assert((routeInformationProvider == null) == (routeInformationParser == null));
/// The [RouteInformationProvider] that is used to configure the [Router].
final RouteInformationProvider? routeInformationProvider;
/// The [RouteInformationParser] that is used to configure the [Router].
final RouteInformationParser<T>? routeInformationParser;
/// The [RouterDelegate] that is used to configure the [Router].
final RouterDelegate<T> routerDelegate;
/// The [BackButtonDispatcher] that is used to configure the [Router].
final BackButtonDispatcher? backButtonDispatcher;
}
RouterDelegate
The router delegate for the router. This delegate consumes the configuration from routeInformationParser and builds a navigating widget for the Router. It is also the primary respondent for the backButtonDispatcher. The Router relies on RouterDelegate.popRoute to handle the back button.If the RouterDelegate.currentConfiguration returns a non-null object, this Router will opt for URL updates.
abstract class RouterDelegate<T> extends Listenable {
//...
// OSによって新しいrouteがpushされたことをrouteInformationProviderからコンフィグ情報として受け取ってこのメソッドが呼ばれる。
Future<void> setNewRoutePath(T configuration);
// https://api.flutter.dev/flutter/widgets/RouterDelegate/build.html
// 最終的にビルドされるウィジェット。
// 通常はNavagatorオブジェクトを返す。
// 基本的にはTのコンフィグ情報を元にして、Navagatorオブジェクトのpagesを構成する。
Widget build(BuildContext context);
}
RouteInformationParser
The route information parser for the router.When the Router gets a new route information from the routeInformationProvider, the Router uses this delegate to parse the route information and produce a configuration. The configuration will be used by routerDelegate and eventually rebuilds the Router widget. Since this delegate is the primary consumer of the routeInformationProvider, it must not be null if routeInformationProvider is not null.
abstract class RouteInformationParser<T> {
//...
// RouteInformation情報をRouterDelegateで処理できるコンフィグ情報(T)にパースする。
// 例えば、RouteInformation.uriをパースして、その内容によってどのページへのルーティングかを判定する、といった処理が行われる。
Future<T> parseRouteInformation(RouteInformation routeInformation) {
//...
}
Future<T> parseRouteInformationWithDependencies(RouteInformation routeInformation, BuildContext context) {
return parseRouteInformation(routeInformation);
}
// コンフィグ情報からRouteInformationを復元する。
RouteInformation? restoreRouteInformation(T configuration) => null;
}
RouteInformatioProvider
The route information provider for the router. The value at the time of first build will be used as the initial route. The Router listens to this provider and rebuilds with new names when it notifies.This can be null if this router does not rely on the route information to build its content. In such case, the routeInformationParser must also be null.
abstract class RouteInformationProvider extends ValueListenable<RouteInformation> {
// 利用方法として、ValueListenable.valueにRouteInformationを持たせ、ChangeNotifierをwithしてnotifyListenerすることにより、リスナーであるRouterが
// RouterInformationをパーサー、デリゲーターへハンドリングする、といった流れになると考えられる。
void routerReportsNewRouteInformation(RouteInformation routeInformation, {RouteInformationReportingType type = RouteInformationReportingType.none}) {}
}
if (widget.routeInformationProvider == null && widget.routeInformationParser != null) {
_defaultRouteInformationProvider ??= PlatformRouteInformationProvider(
initialRouteInformation: RouteInformation(
uri: Uri.parse(_initialRouteName),
),
);
}
backButtonDispatcherは戻るボタンのディスパッチを行う。
(IMO) なぜこのような構成となっているのか?
class RouteInformation {
//...
/// The uri location of the application.
Uri get uri {
if (_uri != null){
return _uri;
}
return Uri.parse(_location!);
}
final Uri? _uri;
/// The state of the application in the [uri].
final Object? state;
}
class _RouterState<T> extends State<Router<T>> with RestorationMixin {
//...
void initState() {
super.initState();
widget.routeInformationProvider?.addListener(_handleRouteInformationProviderNotification);
widget.backButtonDispatcher?.addCallback(_handleBackButtonDispatcherNotification);
widget.routerDelegate.addListener(_handleRouterDelegateNotification);
}
//...
void _handleRouterDelegateNotification() {
setState(() {/* routerDelegate wants to rebuild */});
//...
}
//...
void _handleRouteInformationProviderNotification() {
// ...
// 処理としてはwidget.routeInformationParser!.parseRouteInformationWithDependencies(widget.routeInformationProvider!.value, context)
// -> await widget.routerDelegate.setNewRoutePath(data); -> setStateされる。
// dataはT型のルートのコンフィグ情報
}
//...
Future<bool> _handleBackButtonDispatcherNotification() {
//...
return widget.routerDelegate
.popRoute()
.then<bool>(/* setStateしている */);
}
//...
@override
Widget build(BuildContext context) {
return UnmanagedRestorationScope(
bucket: bucket,
child: _RouterScope(
//...
child: Builder(
builder: widget.routerDelegate.build,
),
),
);
}
}