1
This commit is contained in:
@@ -1,4 +1,2 @@
|
||||
01: 分析 push 方法。
|
||||
A、B、C 三个界面跳转。
|
||||
|
||||
02: replace
|
||||
A、B、C 三个界面跳转。
|
||||
4
lib/09/节点介绍.txt
Normal file
4
lib/09/节点介绍.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
01: 分析 pop 方法。
|
||||
A、B、C 三个界面跳转。
|
||||
|
||||
02: 分析 popUntil 方法。
|
||||
5
lib/10/节点介绍.txt
Normal file
5
lib/10/节点介绍.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
app_info.dart: 弹出对话框界面
|
||||
cupertino_context_menu_demo.dart: 弹出 CupertinoContextMenu
|
||||
dropdown_button_color.dart: 弹出 DropdownButton
|
||||
more_pop_icon.dart: 弹出 PopupMenuButton
|
||||
search_anchor_demo.dart: 弹出 SearchAnchor
|
||||
1
lib/11/节点介绍.txt
Normal file
1
lib/11/节点介绍.txt
Normal file
@@ -0,0 +1 @@
|
||||
路由第二动画控制器效果演示
|
||||
@@ -1,11 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'pages/app/app.dart';
|
||||
|
||||
|
||||
void main() {
|
||||
runApp(const App());
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_strategy/url_strategy.dart';
|
||||
|
||||
import 'pages/app.dart';
|
||||
|
||||
|
||||
|
||||
void main() {
|
||||
setHashUrlStrategy();
|
||||
runApp(const App());
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../route/parser.dart';
|
||||
import '../route/route_state.dart';
|
||||
|
||||
class App extends StatefulWidget {
|
||||
const App({super.key});
|
||||
|
||||
@override
|
||||
State<App> createState() => _AppState();
|
||||
}
|
||||
|
||||
class _AppState extends State<App> {
|
||||
late final RootRouterDelegate _delegate;
|
||||
late final AppRouteParser _parser;
|
||||
late final RouteState _routeState;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_parser = AppRouteParser(initialRoute: '/color', allowPaths: [
|
||||
'/color',
|
||||
'/color/add',
|
||||
'/color/detail/:colorHex',
|
||||
]);
|
||||
_routeState = RouteState(_parser);
|
||||
_delegate = RootRouterDelegate(_routeState);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RouteStateScope(
|
||||
notifier: _routeState,
|
||||
child: MaterialApp.router(
|
||||
routerDelegate: _delegate,
|
||||
routeInformationParser: _parser,
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
appBarTheme: const AppBarTheme(
|
||||
elevation: 0,
|
||||
iconTheme: IconThemeData(color: Colors.black),
|
||||
titleTextStyle: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
))),
|
||||
debugShowCheckedModeBanner: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ColorAddPage extends StatefulWidget {
|
||||
const ColorAddPage({super.key});
|
||||
|
||||
@override
|
||||
State<ColorAddPage> createState() => _ColorAddPageState();
|
||||
}
|
||||
|
||||
class _ColorAddPageState extends State<ColorAddPage> {
|
||||
late Color _color;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_color = randomColor;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String text = '# ${_color.value.toRadixString(16)}';
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('添加颜色'),
|
||||
actions: [IconButton(onPressed: _selectColor, icon: Icon(Icons.check ))],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0,vertical: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Text(text,style: TextStyle(color: _color,fontSize: 24,letterSpacing: 4),)),
|
||||
Container(
|
||||
margin: EdgeInsets.only(left: 10),
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: Icon(
|
||||
Icons.sell_outlined,
|
||||
color: Colors.white,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: _color,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Random _random = Random();
|
||||
|
||||
Color get randomColor {
|
||||
return Color.fromARGB(
|
||||
255,
|
||||
_random.nextInt(256),
|
||||
_random.nextInt(256),
|
||||
_random.nextInt(256),
|
||||
);
|
||||
}
|
||||
|
||||
void _selectColor() {
|
||||
Navigator.of(context).pop(_color);
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../route/route_state.dart';
|
||||
import 'color_add_page.dart';
|
||||
|
||||
import '../../../common/components/colors_panel.dart';
|
||||
import '../../../common/pages/stl_color_page.dart';
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
const HomePage({super.key});
|
||||
|
||||
@override
|
||||
State<HomePage> createState() => _HomePageState();
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> {
|
||||
final List<Color> _colors = [
|
||||
Colors.red, Colors.black, Colors.blue, Colors.green, Colors.orange,
|
||||
Colors.pink, Colors.purple, Colors.indigo, Colors.amber, Colors.cyan,
|
||||
Colors.redAccent, Colors.grey, Colors.blueAccent, Colors.greenAccent, Colors.orangeAccent,
|
||||
Colors.pinkAccent, Colors.purpleAccent, Colors.indigoAccent, Colors.amberAccent, Colors.cyanAccent,
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title:const Text('颜色主页')),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _toAddPage,
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
body: ColorsPanel(
|
||||
colors: _colors,
|
||||
onSelect: _selectColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _selectColor(Color color){
|
||||
String value = '#${color.value.toRadixString(16)}';
|
||||
RouteStateScope.of(context).go('/color/detail/$value');
|
||||
}
|
||||
|
||||
void _toAddPage() async {
|
||||
RouteStateScope.of(context).go('/color/add');
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:quiver/core.dart';
|
||||
|
||||
/// A route path that has been parsed by [TemplateRouteParser].
|
||||
class ParsedRoute extends Equatable{
|
||||
/// The current path location without query parameters. (/book/123)
|
||||
final String path;
|
||||
|
||||
/// The path template (/book/:id)
|
||||
final String pathTemplate;
|
||||
|
||||
/// The path parameters ({id: 123})
|
||||
final Map<String, String> parameters;
|
||||
|
||||
/// The query parameters ({search: abc})
|
||||
final Map<String, String> queryParameters;
|
||||
|
||||
const ParsedRoute(this.path, this.pathTemplate, this.parameters, this.queryParameters);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [path,pathTemplate,parameters,queryParameters];
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:iroute/common/pages/stl_color_page.dart';
|
||||
import 'package:path_to_regexp/path_to_regexp.dart';
|
||||
import '../pages/home_page.dart';
|
||||
import '../pages/color_add_page.dart';
|
||||
import 'parsed_route.dart';
|
||||
import 'route_state.dart';
|
||||
|
||||
class AppRouteParser extends RouteInformationParser<ParsedRoute> {
|
||||
final List<String> allowPaths;
|
||||
final ParsedRoute initialRoute;
|
||||
|
||||
AppRouteParser({
|
||||
required this.allowPaths,
|
||||
String initialRoute = '/',
|
||||
}):initialRoute = ParsedRoute(initialRoute, initialRoute, {}, {});
|
||||
|
||||
@override
|
||||
Future<ParsedRoute> parseRouteInformation(
|
||||
RouteInformation routeInformation) async {
|
||||
print("=======parseRouteInformation:${routeInformation.uri.path}===================");
|
||||
|
||||
final uri = routeInformation.uri;
|
||||
final path = uri.toString();
|
||||
final queryParams = uri.queryParameters;
|
||||
|
||||
ParsedRoute parsedRoute = initialRoute;
|
||||
|
||||
for (var pathTemplate in allowPaths) {
|
||||
final parameters = <String>[];
|
||||
var pathRegExp = pathToRegExp(pathTemplate, parameters: parameters);
|
||||
if (pathRegExp.hasMatch(path)) {
|
||||
final match = pathRegExp.matchAsPrefix(path);
|
||||
if (match == null) continue;
|
||||
final params = extract(parameters, match);
|
||||
parsedRoute = ParsedRoute(path, pathTemplate, params, queryParams);
|
||||
}
|
||||
}
|
||||
return parsedRoute;
|
||||
}
|
||||
|
||||
@override
|
||||
RouteInformation? restoreRouteInformation(ParsedRoute configuration) {
|
||||
print("=======restoreRouteInformation:${configuration}===================");
|
||||
|
||||
return RouteInformation(uri: Uri.parse(configuration.path));
|
||||
}
|
||||
}
|
||||
|
||||
class RootRouterDelegate extends RouterDelegate<ParsedRoute>
|
||||
with ChangeNotifier, PopNavigatorRouterDelegateMixin {
|
||||
|
||||
final RouteState routeState;
|
||||
|
||||
|
||||
RootRouterDelegate(this.routeState) {
|
||||
routeState.addListener(notifyListeners);
|
||||
}
|
||||
|
||||
@override
|
||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
@override
|
||||
ParsedRoute get currentConfiguration {
|
||||
print("=======currentConfiguration:${routeState.route.path}===================");
|
||||
|
||||
return routeState.route;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
routeState.removeListener(notifyListeners);
|
||||
routeState.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
final _addColorKey = const ValueKey('addColor');
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final routeState = RouteStateScope.of(context);
|
||||
List<Page> pages =[];
|
||||
final String pathTemplate = routeState.route.pathTemplate;
|
||||
|
||||
pages.add(MaterialPage(child: HomePage()));
|
||||
|
||||
String? selectedColor;
|
||||
if (pathTemplate == '/color/detail/:colorHex') {
|
||||
selectedColor = routeState.route.parameters['colorHex'];
|
||||
}
|
||||
|
||||
if(selectedColor!=null){
|
||||
pages.add(MaterialPage(child: StlColorPage(
|
||||
color: Colors.redAccent,
|
||||
)));
|
||||
}
|
||||
|
||||
bool isAddPage = pathTemplate == '/color/add';
|
||||
if(isAddPage){
|
||||
pages.add(MaterialPage(
|
||||
key: _addColorKey,
|
||||
child: ColorAddPage()));
|
||||
}
|
||||
print("===build====================");
|
||||
return Navigator(
|
||||
key: navigatorKey,
|
||||
pages: pages,
|
||||
onPopPage: (route, result) {
|
||||
print(result);
|
||||
if (route.settings is Page &&
|
||||
(route.settings as Page).key == _addColorKey) {
|
||||
routeState.go('/color');
|
||||
}
|
||||
// appState.selectedBook = null;
|
||||
// notifyListeners();
|
||||
return route.didPop(result);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 格式: /a /b
|
||||
|
||||
@override
|
||||
Future<void> setNewRoutePath(ParsedRoute configuration) async {
|
||||
print("===setNewRoutePath===${configuration}=================");
|
||||
routeState.route = configuration;
|
||||
return;
|
||||
}
|
||||
|
||||
// @override
|
||||
// Future<void> setInitialRoutePath( configuration) async{
|
||||
// _pages = [_pageMap['/']!];
|
||||
// notifyListeners();
|
||||
// }
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'parsed_route.dart';
|
||||
import 'parser.dart';
|
||||
|
||||
/// The current route state. To change the current route, call obtain the state
|
||||
/// using `RouteStateScope.of(context)` and call `go()`:
|
||||
///
|
||||
/// ```
|
||||
/// RouteStateScope.of(context).go('/book/2');
|
||||
/// ```
|
||||
class RouteState extends ChangeNotifier {
|
||||
final AppRouteParser _parser;
|
||||
ParsedRoute _route;
|
||||
|
||||
RouteState(this._parser) : _route = _parser.initialRoute;
|
||||
|
||||
ParsedRoute get route => _route;
|
||||
|
||||
set route(ParsedRoute route) {
|
||||
// Don't notify listeners if the path hasn't changed.
|
||||
if (_route == route) return;
|
||||
|
||||
_route = route;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> go(String route) async {
|
||||
this.route = await _parser
|
||||
.parseRouteInformation(RouteInformation(uri: Uri.parse(route)));
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides the current [RouteState] to descendant widgets in the tree.
|
||||
class RouteStateScope extends InheritedNotifier<RouteState> {
|
||||
const RouteStateScope({
|
||||
required super.notifier,
|
||||
required super.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
static RouteState of(BuildContext context) =>
|
||||
context.dependOnInheritedWidgetOfExactType<RouteStateScope>()!.notifier!;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_strategy/url_strategy.dart';
|
||||
|
||||
import 'pages/app.dart';
|
||||
|
||||
|
||||
|
||||
void main() {
|
||||
setHashUrlStrategy();
|
||||
runApp(const App());
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../route/parser.dart';
|
||||
import '../route/route_state.dart';
|
||||
|
||||
class App extends StatefulWidget {
|
||||
const App({super.key});
|
||||
|
||||
@override
|
||||
State<App> createState() => _AppState();
|
||||
}
|
||||
|
||||
class _AppState extends State<App> {
|
||||
late final RootRouterDelegate _delegate;
|
||||
late final AppRouteParser _parser;
|
||||
late final RouteState _routeState;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_parser = AppRouteParser();
|
||||
_routeState = RouteState(_parser,initial: '/color');
|
||||
_delegate = RootRouterDelegate(_routeState);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RouteStateScope(
|
||||
notifier: _routeState,
|
||||
child: MaterialApp.router(
|
||||
routerDelegate: _delegate,
|
||||
routeInformationParser: _parser,
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
appBarTheme: const AppBarTheme(
|
||||
elevation: 0,
|
||||
iconTheme: IconThemeData(color: Colors.black),
|
||||
titleTextStyle: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
))),
|
||||
debugShowCheckedModeBanner: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ColorAddPage extends StatefulWidget {
|
||||
const ColorAddPage({super.key});
|
||||
|
||||
@override
|
||||
State<ColorAddPage> createState() => _ColorAddPageState();
|
||||
}
|
||||
|
||||
class _ColorAddPageState extends State<ColorAddPage> {
|
||||
late Color _color;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_color = randomColor;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String text = '# ${_color.value.toRadixString(16)}';
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('添加颜色'),
|
||||
actions: [IconButton(onPressed: _selectColor, icon: Icon(Icons.check ))],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0,vertical: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Text(text,style: TextStyle(color: _color,fontSize: 24,letterSpacing: 4),)),
|
||||
Container(
|
||||
margin: EdgeInsets.only(left: 10),
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: Icon(
|
||||
Icons.sell_outlined,
|
||||
color: Colors.white,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: _color,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Random _random = Random();
|
||||
|
||||
Color get randomColor {
|
||||
return Color.fromARGB(
|
||||
255,
|
||||
_random.nextInt(256),
|
||||
_random.nextInt(256),
|
||||
_random.nextInt(256),
|
||||
);
|
||||
}
|
||||
|
||||
void _selectColor() {
|
||||
Navigator.of(context).pop(_color);
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../route/route_state.dart';
|
||||
import 'color_add_page.dart';
|
||||
|
||||
import '../../../common/components/colors_panel.dart';
|
||||
import '../../../common/pages/stl_color_page.dart';
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
const HomePage({super.key});
|
||||
|
||||
@override
|
||||
State<HomePage> createState() => _HomePageState();
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> {
|
||||
final List<Color> _colors = [
|
||||
Colors.red, Colors.black, Colors.blue, Colors.green, Colors.orange,
|
||||
Colors.pink, Colors.purple, Colors.indigo, Colors.amber, Colors.cyan,
|
||||
Colors.redAccent, Colors.grey, Colors.blueAccent, Colors.greenAccent, Colors.orangeAccent,
|
||||
Colors.pinkAccent, Colors.purpleAccent, Colors.indigoAccent, Colors.amberAccent, Colors.cyanAccent,
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title:const Text('颜色主页')),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _toAddPage,
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
body: ColorsPanel(
|
||||
colors: _colors,
|
||||
onSelect: _selectColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _selectColor(Color color){
|
||||
String value = color.value.toRadixString(16);
|
||||
RouteStateScope.of(context).go('/color/detail?color=$value');
|
||||
}
|
||||
|
||||
void _toAddPage() async {
|
||||
RouteStateScope.of(context).go('/color/add');
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:quiver/core.dart';
|
||||
|
||||
/// A route path that has been parsed by [TemplateRouteParser].
|
||||
class ParsedRoute extends Equatable{
|
||||
/// The current path location without query parameters. (/book/123)
|
||||
final String path;
|
||||
|
||||
/// The query parameters ({search: abc})
|
||||
final Map<String, String> queryParameters;
|
||||
|
||||
const ParsedRoute(this.path, this.queryParameters);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [path,queryParameters];
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:iroute/common/pages/stl_color_page.dart';
|
||||
import 'package:path_to_regexp/path_to_regexp.dart';
|
||||
import '../pages/home_page.dart';
|
||||
import '../pages/color_add_page.dart';
|
||||
import 'parsed_route.dart';
|
||||
import 'route_state.dart';
|
||||
|
||||
class AppRouteParser extends RouteInformationParser<ParsedRoute> {
|
||||
|
||||
@override
|
||||
Future<ParsedRoute> parseRouteInformation(
|
||||
RouteInformation routeInformation) async {
|
||||
print("=======parseRouteInformation:${routeInformation.uri.path}===================");
|
||||
|
||||
final uri = routeInformation.uri;
|
||||
final path = uri.toString();
|
||||
final queryParams = uri.queryParameters;
|
||||
|
||||
return ParsedRoute(path, queryParams);
|
||||
}
|
||||
|
||||
@override
|
||||
RouteInformation? restoreRouteInformation(ParsedRoute configuration) {
|
||||
print("=======restoreRouteInformation:${configuration}===================");
|
||||
|
||||
return RouteInformation(uri: Uri.parse(configuration.path));
|
||||
}
|
||||
}
|
||||
|
||||
class RootRouterDelegate extends RouterDelegate<ParsedRoute>
|
||||
with ChangeNotifier, PopNavigatorRouterDelegateMixin {
|
||||
|
||||
final RouteState routeState;
|
||||
|
||||
|
||||
RootRouterDelegate(this.routeState) {
|
||||
routeState.addListener(notifyListeners);
|
||||
}
|
||||
|
||||
@override
|
||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
@override
|
||||
ParsedRoute get currentConfiguration {
|
||||
print("=======currentConfiguration:${routeState.route.path}===================");
|
||||
|
||||
return routeState.route;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
routeState.removeListener(notifyListeners);
|
||||
routeState.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
final _addColorKey = const ValueKey('addColor');
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final routeState = RouteStateScope.of(context);
|
||||
List<Page> pages =[];
|
||||
final String path = routeState.route.path;
|
||||
|
||||
pages.add(MaterialPage(child: HomePage()));
|
||||
|
||||
String? selectedColor;
|
||||
if (path.startsWith('/color/detail') ) {
|
||||
selectedColor = routeState.route.queryParameters['color'];
|
||||
}
|
||||
|
||||
if(selectedColor!=null){
|
||||
pages.add(MaterialPage(child: StlColorPage(
|
||||
color: Color(int.parse(selectedColor,radix: 16)),
|
||||
)));
|
||||
}
|
||||
|
||||
bool isAddPage = path == '/color/add';
|
||||
if(isAddPage){
|
||||
pages.add(MaterialPage(
|
||||
key: _addColorKey,
|
||||
child: ColorAddPage()));
|
||||
}
|
||||
|
||||
return Navigator(
|
||||
key: navigatorKey,
|
||||
pages: pages,
|
||||
onPopPage: (route, result) {
|
||||
if (route.settings is Page && (route.settings as Page).key == _addColorKey) {
|
||||
routeState.go('/color');
|
||||
}
|
||||
|
||||
if(selectedColor!=null){
|
||||
selectedColor = null;
|
||||
routeState.go('/color');
|
||||
}
|
||||
|
||||
return route.didPop(result);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 格式: /a /b
|
||||
|
||||
@override
|
||||
Future<void> setNewRoutePath(ParsedRoute configuration) async {
|
||||
print("===setNewRoutePath===${configuration}=================");
|
||||
routeState.route = configuration;
|
||||
return;
|
||||
}
|
||||
|
||||
// @override
|
||||
// Future<void> setInitialRoutePath( configuration) async{
|
||||
// _pages = [_pageMap['/']!];
|
||||
// notifyListeners();
|
||||
// }
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'parsed_route.dart';
|
||||
import 'parser.dart';
|
||||
|
||||
/// The current route state. To change the current route, call obtain the state
|
||||
/// using `RouteStateScope.of(context)` and call `go()`:
|
||||
///
|
||||
/// ```
|
||||
/// RouteStateScope.of(context).go('/book/2');
|
||||
/// ```
|
||||
class RouteState extends ChangeNotifier {
|
||||
final AppRouteParser _parser;
|
||||
ParsedRoute _route;
|
||||
|
||||
RouteState(this._parser,{String initial=''}) : _route = ParsedRoute(initial,{});
|
||||
|
||||
ParsedRoute get route => _route;
|
||||
|
||||
set route(ParsedRoute route) {
|
||||
// Don't notify listeners if the path hasn't changed.
|
||||
if (_route == route) return;
|
||||
|
||||
_route = route;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> go(String route) async {
|
||||
this.route = await _parser
|
||||
.parseRouteInformation(RouteInformation(uri: Uri.parse(route)));
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides the current [RouteState] to descendant widgets in the tree.
|
||||
class RouteStateScope extends InheritedNotifier<RouteState> {
|
||||
const RouteStateScope({
|
||||
required super.notifier,
|
||||
required super.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
static RouteState of(BuildContext context) =>
|
||||
context.dependOnInheritedWidgetOfExactType<RouteStateScope>()!.notifier!;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_strategy/url_strategy.dart';
|
||||
|
||||
import 'pages/app.dart';
|
||||
|
||||
|
||||
|
||||
void main() {
|
||||
setHashUrlStrategy();
|
||||
runApp(const App());
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../route/parser.dart';
|
||||
import '../route/route_state.dart';
|
||||
|
||||
class App extends StatefulWidget {
|
||||
const App({super.key});
|
||||
|
||||
@override
|
||||
State<App> createState() => _AppState();
|
||||
}
|
||||
|
||||
AppRouteParser parser = AppRouteParser();
|
||||
AppRouterDelegate routerDelegate = AppRouterDelegate();
|
||||
|
||||
class _AppState extends State<App> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp.router(
|
||||
routerDelegate: routerDelegate,
|
||||
routeInformationParser: parser,
|
||||
// backButtonDispatcher: RootBackButtonDispatcher(),
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
appBarTheme: const AppBarTheme(
|
||||
elevation: 0,
|
||||
iconTheme: IconThemeData(color: Colors.black),
|
||||
titleTextStyle: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
))),
|
||||
debugShowCheckedModeBanner: false,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ColorAddPage extends StatefulWidget {
|
||||
const ColorAddPage({super.key});
|
||||
|
||||
@override
|
||||
State<ColorAddPage> createState() => _ColorAddPageState();
|
||||
}
|
||||
|
||||
class _ColorAddPageState extends State<ColorAddPage> {
|
||||
late Color _color;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_color = randomColor;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String text = '# ${_color.value.toRadixString(16)}';
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('添加颜色'),
|
||||
actions: [IconButton(onPressed: _selectColor, icon: Icon(Icons.check ))],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0,vertical: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Text(text,style: TextStyle(color: _color,fontSize: 24,letterSpacing: 4),)),
|
||||
Container(
|
||||
margin: EdgeInsets.only(left: 10),
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: Icon(
|
||||
Icons.sell_outlined,
|
||||
color: Colors.white,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: _color,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Random _random = Random();
|
||||
|
||||
Color get randomColor {
|
||||
return Color.fromARGB(
|
||||
255,
|
||||
_random.nextInt(256),
|
||||
_random.nextInt(256),
|
||||
_random.nextInt(256),
|
||||
);
|
||||
}
|
||||
|
||||
void _selectColor() {
|
||||
Navigator.of(context).pop(_color);
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../route/route_state.dart';
|
||||
import 'app.dart';
|
||||
import 'color_add_page.dart';
|
||||
|
||||
import '../../../common/components/colors_panel.dart';
|
||||
import '../../../common/pages/stl_color_page.dart';
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
const HomePage({super.key});
|
||||
|
||||
@override
|
||||
State<HomePage> createState() => _HomePageState();
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> {
|
||||
final List<Color> _colors = [
|
||||
Colors.red, Colors.black, Colors.blue, Colors.green, Colors.orange,
|
||||
Colors.pink, Colors.purple, Colors.indigo, Colors.amber, Colors.cyan,
|
||||
Colors.redAccent, Colors.grey, Colors.blueAccent, Colors.greenAccent, Colors.orangeAccent,
|
||||
Colors.pinkAccent, Colors.purpleAccent, Colors.indigoAccent, Colors.amberAccent, Colors.cyanAccent,
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title:const Text('颜色主页')),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _toAddPage,
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
body: ColorsPanel(
|
||||
colors: _colors,
|
||||
onSelect: _selectColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _selectColor(Color color){
|
||||
String value = color.value.toRadixString(16);
|
||||
routerDelegate.go('/color/detail?color=$value');
|
||||
}
|
||||
|
||||
void _toAddPage() async {
|
||||
routerDelegate.go('/color/add');
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:quiver/core.dart';
|
||||
|
||||
/// A route path that has been parsed by [TemplateRouteParser].
|
||||
class ParsedRoute extends Equatable{
|
||||
/// The current path location without query parameters. (/book/123)
|
||||
final String path;
|
||||
|
||||
/// The query parameters ({search: abc})
|
||||
final Map<String, String> queryParameters;
|
||||
|
||||
const ParsedRoute(this.path, this.queryParameters);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [path,queryParameters];
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:iroute/common/pages/stl_color_page.dart';
|
||||
import 'package:path_to_regexp/path_to_regexp.dart';
|
||||
import '../pages/app.dart';
|
||||
import '../pages/home_page.dart';
|
||||
import '../pages/color_add_page.dart';
|
||||
import 'parsed_route.dart';
|
||||
import 'route_state.dart';
|
||||
|
||||
class AppRouteParser extends RouteInformationParser<ParsedRoute> {
|
||||
@override
|
||||
Future<ParsedRoute> parseRouteInformation(
|
||||
RouteInformation routeInformation) async {
|
||||
print("=======parseRouteInformation:${routeInformation.uri.path}===================");
|
||||
|
||||
final uri = routeInformation.uri;
|
||||
final path = uri.toString();
|
||||
final queryParams = uri.queryParameters;
|
||||
|
||||
return ParsedRoute(path, queryParams);
|
||||
}
|
||||
|
||||
@override
|
||||
RouteInformation? restoreRouteInformation(ParsedRoute configuration) {
|
||||
print("=======restoreRouteInformation:${configuration}===================");
|
||||
return RouteInformation(uri: Uri.parse(configuration.path));
|
||||
}
|
||||
}
|
||||
|
||||
class AppRouterDelegate extends RouterDelegate<ParsedRoute>
|
||||
with ChangeNotifier, PopNavigatorRouterDelegateMixin {
|
||||
|
||||
AppRouterDelegate({String initial = '/'}) {
|
||||
_routes.add(ParsedRoute(initial, {}));
|
||||
}
|
||||
|
||||
final List<ParsedRoute> _routes = [];
|
||||
|
||||
@override
|
||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
@override
|
||||
ParsedRoute get currentConfiguration {
|
||||
return _routes.last;
|
||||
}
|
||||
|
||||
Future<void> go(String route) async {
|
||||
ParsedRoute _route = await parser.parseRouteInformation(RouteInformation(uri: Uri.parse(route)));
|
||||
if(_route!=_routes.last){
|
||||
_routes.add(_route);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
final _addColorKey = const ValueKey('addColor');
|
||||
final _colorDetailKey = const ValueKey('ColorDetail');
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Page> pages = [];
|
||||
|
||||
for (ParsedRoute route in _routes) {
|
||||
Page? page = _buildPageByPath(route);
|
||||
if(page!=null){
|
||||
pages.add(page);
|
||||
}
|
||||
}
|
||||
|
||||
return Navigator(
|
||||
key: navigatorKey,
|
||||
pages: pages,
|
||||
onPopPage: _onPagePop,
|
||||
);
|
||||
}
|
||||
|
||||
Page? _buildPageByPath(ParsedRoute route) {
|
||||
if(route.path == '/'){
|
||||
return MaterialPage(child: HomePage());
|
||||
}
|
||||
|
||||
if(route.path.startsWith('/color/detail')){
|
||||
String? selectedColor = route.queryParameters['color'];
|
||||
|
||||
if (selectedColor != null) {
|
||||
Color color = Color(int.parse(selectedColor, radix: 16));
|
||||
return MaterialPage(key: _colorDetailKey, child: StlColorPage(color: color));
|
||||
}
|
||||
}
|
||||
|
||||
if (route.path == '/color/add') {
|
||||
return MaterialPage(key: _addColorKey, child: ColorAddPage());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setNewRoutePath(ParsedRoute configuration) async {
|
||||
print("===setNewRoutePath===${configuration}=================");
|
||||
if(configuration!=_routes.last){
|
||||
_routes.add(configuration);
|
||||
}
|
||||
}
|
||||
|
||||
bool _onPagePop(Route route, result) {
|
||||
RouteSettings settings = route.settings;
|
||||
if(settings is Page){
|
||||
if(settings.key==_addColorKey){
|
||||
_routes.removeLast();
|
||||
notifyListeners();
|
||||
}
|
||||
if(settings.key==_colorDetailKey){
|
||||
_routes.removeLast();
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
return route.didPop(result);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'parsed_route.dart';
|
||||
import 'parser.dart';
|
||||
|
||||
/// The current route state. To change the current route, call obtain the state
|
||||
/// using `RouteStateScope.of(context)` and call `go()`:
|
||||
///
|
||||
/// ```
|
||||
/// RouteStateScope.of(context).go('/book/2');
|
||||
/// ```
|
||||
class RouteState extends ChangeNotifier {
|
||||
final AppRouteParser _parser;
|
||||
ParsedRoute _route;
|
||||
|
||||
RouteState(this._parser,{String initial=''}) : _route = ParsedRoute(initial,{});
|
||||
|
||||
ParsedRoute get route => _route;
|
||||
|
||||
set route(ParsedRoute route) {
|
||||
// Don't notify listeners if the path hasn't changed.
|
||||
if (_route == route) return;
|
||||
|
||||
_route = route;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> go(String route) async {
|
||||
this.route = await _parser
|
||||
.parseRouteInformation(RouteInformation(uri: Uri.parse(route)));
|
||||
}
|
||||
}
|
||||
|
||||
// /// Provides the current [RouteState] to descendant widgets in the tree.
|
||||
// class RouteStateScope extends InheritedNotifier<RouteState> {
|
||||
// const RouteStateScope({
|
||||
// required super.notifier,
|
||||
// required super.child,
|
||||
// super.key,
|
||||
// });
|
||||
//
|
||||
// static RouteState of(BuildContext context) =>
|
||||
// context.dependOnInheritedWidgetOfExactType<RouteStateScope>()!.notifier!;
|
||||
// }
|
||||
@@ -1,14 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'pages/app/app.dart';
|
||||
import 'pages/app/app_router_delegate.dart';
|
||||
|
||||
|
||||
AppRouterDelegate router = AppRouterDelegate();
|
||||
|
||||
void main() {
|
||||
runApp(const App());
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
import '../../main.dart';
|
||||
import 'app_tool_bar.dart';
|
||||
|
||||
class App extends StatelessWidget {
|
||||
const App({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
theme: ThemeData(
|
||||
appBarTheme: const AppBarTheme(
|
||||
elevation: 0,
|
||||
iconTheme: IconThemeData(color: Colors.black),
|
||||
titleTextStyle: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
))),
|
||||
debugShowCheckedModeBanner: false,
|
||||
home: Scaffold(
|
||||
appBar: AppToolBar(),
|
||||
body: Router(routerDelegate: router),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../page_a.dart';
|
||||
import '../page_b.dart';
|
||||
import '../page_c.dart';
|
||||
import '../home_page.dart';
|
||||
|
||||
class AppRouterDelegate extends RouterDelegate<String> with ChangeNotifier, PopNavigatorRouterDelegateMixin {
|
||||
|
||||
AppRouterDelegate({String initial = '/'}):_path=initial;
|
||||
|
||||
String _path;
|
||||
|
||||
String get path=> _path;
|
||||
|
||||
void go(String path){
|
||||
_path = path;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Navigator(
|
||||
onPopPage: _onPopPage,
|
||||
pages: parserConfig(_path).map((e) => _pageMap[e]!).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
final Map<String, Page> _pageMap = const {
|
||||
'/': MaterialPage(child: HomePage()),
|
||||
'a': MaterialPage(child: PageA()),
|
||||
'b': MaterialPage(child: PageB()),
|
||||
'c': MaterialPage(child: PageC()),
|
||||
};
|
||||
|
||||
Widget buildNavigatorByConfig(String path) {
|
||||
return Navigator(
|
||||
onPopPage: _onPopPage,
|
||||
pages: parserConfig(path).map((e) => _pageMap[e]!).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
bool _onPopPage(Route route, result) {
|
||||
if(path.length>1){
|
||||
int last = path.lastIndexOf('/');
|
||||
_path = path.substring(0,last);
|
||||
if(_path.isEmpty) _path = '/';
|
||||
notifyListeners();
|
||||
}
|
||||
return route.didPop(result);
|
||||
}
|
||||
|
||||
@override
|
||||
GlobalKey<NavigatorState>? navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
|
||||
@override
|
||||
Future<void> setNewRoutePath(String configuration) async{
|
||||
_path = configuration;
|
||||
}
|
||||
|
||||
List<String> parserConfig(String path){
|
||||
/// 例: /a/b/c 进栈 /,a,b,c 界面
|
||||
List<String> result = ['/'];
|
||||
if(path.startsWith('/')){
|
||||
path = path.substring(1);
|
||||
if(path.isNotEmpty){
|
||||
List<String> parts = path.split('/');
|
||||
result.addAll(parts);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../main.dart';
|
||||
|
||||
class AppToolBar extends StatefulWidget implements PreferredSizeWidget{
|
||||
const AppToolBar({super.key});
|
||||
|
||||
@override
|
||||
State<AppToolBar> createState() => _AppToolBarState();
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||
}
|
||||
|
||||
class _AppToolBarState extends State<AppToolBar> {
|
||||
|
||||
TextEditingController _ctrl = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_changRoute();
|
||||
router.addListener(_changRoute);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
router.removeListener(_changRoute);
|
||||
_ctrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _changRoute() {
|
||||
_ctrl.text=router.path;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
return AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
title : TextField(
|
||||
controller: _ctrl,
|
||||
onSubmitted: _onSubmitted,
|
||||
decoration: InputDecoration( //装饰
|
||||
filled: true, //填充
|
||||
fillColor: Color(0xffF3F6F9), //填充颜色
|
||||
constraints: BoxConstraints(maxHeight: 34), //约束信息
|
||||
contentPadding: EdgeInsets.only(top: -14,left: 10),
|
||||
border: UnderlineInputBorder( //边线信息
|
||||
borderSide: BorderSide.none,
|
||||
borderRadius: BorderRadius.all(Radius.circular(6)),
|
||||
),
|
||||
hintText: "请输入路由", //提示字
|
||||
hintStyle: TextStyle(fontSize: 14) //提示字样式
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onSubmitted(String value) {
|
||||
router.go(value);
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../main.dart';
|
||||
|
||||
|
||||
class HomePage extends StatelessWidget {
|
||||
const HomePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const Color bgColor = Color(0xffCCFFCC);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: bgColor,
|
||||
appBar: AppBar(
|
||||
title: const Text('主页起点'),
|
||||
backgroundColor: bgColor,
|
||||
),
|
||||
body: Center(child: ElevatedButton(
|
||||
onPressed: () => toPageA(context),
|
||||
child: const Text('Push A'),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
void toPageA(BuildContext context) {
|
||||
router.go('/a');
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../main.dart';
|
||||
|
||||
class PageA extends StatelessWidget {
|
||||
const PageA({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const Color bgColor = Color(0xffCCFFFF);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: bgColor,
|
||||
appBar: AppBar(
|
||||
title: const Text('A 界面'),
|
||||
backgroundColor: bgColor,
|
||||
),
|
||||
body: Center(
|
||||
child: ElevatedButton(
|
||||
onPressed: () => toPageB(context),
|
||||
child: const Text('Push B'),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
void toPageB(BuildContext context) {
|
||||
// router.value = ['/', 'a', 'b'];
|
||||
router.go('/a/b');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../main.dart';
|
||||
|
||||
class PageB extends StatelessWidget {
|
||||
const PageB({super.key});
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const Color bgColor = Color(0xffCCE5FF);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: bgColor,
|
||||
appBar: AppBar(
|
||||
title: const Text('B 界面'),
|
||||
backgroundColor: bgColor,
|
||||
|
||||
),
|
||||
body: Center(
|
||||
child: ElevatedButton(
|
||||
onPressed: () => toPageC(context),
|
||||
child: const Text('Push C'),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
void toPageC(BuildContext context){
|
||||
router.go('/a/b/c');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../main.dart';
|
||||
|
||||
class PageC extends StatelessWidget {
|
||||
const PageC({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const Color bgColor = Color(0xffFFE6CC);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: bgColor,
|
||||
appBar: AppBar(
|
||||
title: const Text('C 界面'),
|
||||
backgroundColor: bgColor,
|
||||
),
|
||||
body: Center(
|
||||
child: Text('到达终点'),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'pages/app/app.dart';
|
||||
|
||||
|
||||
|
||||
void main() {
|
||||
runApp(const App());
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import 'package:toly_menu/toly_menu.dart';
|
||||
|
||||
List<MenuData> menus = [
|
||||
MenuData(path: '/dashboard', label: '总览面板', children: [
|
||||
MenuData(path: '/dashboard/data_analyse', label: '数据分析', deep: 1),
|
||||
MenuData(path: '/dashboard/work_board', label: '工作台', deep: 1, children: [
|
||||
MenuData(path: '/dashboard/work_board/a', label: '第一工作区', deep: 2),
|
||||
MenuData(path: '/dashboard/work_board/b', label: '第二工作区', deep: 2),
|
||||
MenuData(path: '/dashboard/work_board/c', label: '第三工作区', deep: 2),
|
||||
]),
|
||||
]),
|
||||
MenuData(path: '/cases', label: '案例演示', children: [
|
||||
MenuData(path: '/cases/counter', label: '计数器项目', deep: 1),
|
||||
MenuData(path: '/cases/guess', label: '猜数字项目', deep: 1),
|
||||
MenuData(path: '/cases/muyu', label: '电子木鱼项目', deep: 1),
|
||||
MenuData(path: '/cases/canvas', label: '白板绘制项目', deep: 1),
|
||||
MenuData(path: '/cases/timer', label: '计时器项目', deep: 1),
|
||||
]),
|
||||
MenuData(path: '/manager', label: '系统管理', children: [
|
||||
MenuData(path: '/manager/account', label: '账号管理', deep: 1),
|
||||
MenuData(path: '/manager/role', label: '角色管理', deep: 1),
|
||||
MenuData(path: '/manager/menu', label: '菜单管理', deep: 1),
|
||||
]),
|
||||
MenuData(path: '/knowledge', label: '知识库管理', children: [
|
||||
MenuData(path: '/knowledge/a', label: '语文知识', deep: 1, children: [
|
||||
MenuData(
|
||||
path: '/knowledge/a/1',
|
||||
label: '诗词歌赋',
|
||||
deep: 2,
|
||||
),
|
||||
MenuData(
|
||||
path: '/knowledge/a/2',
|
||||
label: '人物故事',
|
||||
deep: 2,
|
||||
),
|
||||
MenuData(
|
||||
path: '/knowledge/a/3',
|
||||
label: '名著推荐',
|
||||
deep: 2,
|
||||
)
|
||||
]),
|
||||
MenuData(
|
||||
path: '/knowledge/b',
|
||||
label: '数学知识',
|
||||
children: [
|
||||
MenuData(
|
||||
path: '/knowledge/b/1',
|
||||
label: '人物故事',
|
||||
deep: 2,
|
||||
),
|
||||
MenuData(
|
||||
path: '/knowledge/b/2',
|
||||
label: '数学定理',
|
||||
deep: 2,
|
||||
),
|
||||
MenuData(
|
||||
path: '/knowledge/b/3',
|
||||
label: '几何知识',
|
||||
deep: 2,
|
||||
),
|
||||
MenuData(
|
||||
path: '/knowledge/b/4',
|
||||
label: '代数知识',
|
||||
deep: 2,
|
||||
)
|
||||
],
|
||||
deep: 1),
|
||||
MenuData(
|
||||
path: '/knowledge/c',
|
||||
label: '英语知识',
|
||||
deep: 1,
|
||||
),
|
||||
]),
|
||||
MenuData(path: '/widgets', label: '组件集录', children: [
|
||||
MenuData(path: '/widgets/stateless', label: '无状态组件', deep: 1),
|
||||
MenuData(path: '/widgets/stateful', label: '有状态组件', deep: 1),
|
||||
MenuData(path: '/widgets/single_child', label: '单子渲染组件', deep: 1),
|
||||
MenuData(path: '/widgets/mutli_child', label: '多子渲染组件', deep: 1),
|
||||
MenuData(path: '/widgets/sliver', label: '滑片组件', deep: 1),
|
||||
MenuData(path: '/widgets/proxy', label: '代理组件', deep: 1),
|
||||
MenuData(path: '/widgets/other', label: '其他组件', deep: 1),
|
||||
])
|
||||
];
|
||||
@@ -1,107 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:toly_menu/toly_menu.dart';
|
||||
|
||||
import '../../menus.dart';
|
||||
import '../../transition/fade_transition.dart';
|
||||
import 'root_content.dart';
|
||||
|
||||
class App extends StatelessWidget {
|
||||
const App({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
theme: ThemeData(
|
||||
pageTransitionsTheme: PageTransitionsTheme(
|
||||
builders: {
|
||||
TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
|
||||
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
|
||||
TargetPlatform.macOS: FadeUpwardsPageTransitionsBuilder(),
|
||||
TargetPlatform.windows: FadeTransitionsBuilder(),
|
||||
}
|
||||
),
|
||||
|
||||
appBarTheme: const AppBarTheme(
|
||||
elevation: 0,
|
||||
iconTheme: IconThemeData(color: Colors.black),
|
||||
backgroundColor: Colors.white,
|
||||
titleTextStyle: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
))),
|
||||
debugShowCheckedModeBanner: false,
|
||||
home: AppLayout());
|
||||
}
|
||||
}
|
||||
|
||||
class AppLayout extends StatelessWidget {
|
||||
const AppLayout({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 210,
|
||||
child: MenuPanel(),
|
||||
),
|
||||
// Container()
|
||||
Expanded(child: RootContent())
|
||||
],
|
||||
),
|
||||
// appBar: AppToolBar(),
|
||||
// body: AppNavigation(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MenuPanel extends StatefulWidget {
|
||||
const MenuPanel({super.key});
|
||||
|
||||
@override
|
||||
State<MenuPanel> createState() => _MenuPanelState();
|
||||
}
|
||||
|
||||
class _MenuPanelState extends State<MenuPanel> {
|
||||
MenuState state = MenuState(expandMenus: ['/dashboard'], activeMenu: '/dashboard/data_analyse', items: menus);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TolyMenu(state: state, onSelect: _onSelect);
|
||||
}
|
||||
|
||||
void _onSelect(MenuData menu) {
|
||||
|
||||
|
||||
if(menu.isLeaf){
|
||||
state = state.copyWith(activeMenu: menu.path);
|
||||
print("menu.path:${menu.path}");
|
||||
goRouter.go(menu.path);
|
||||
}else{
|
||||
List<String> menus = [];
|
||||
String path = menu.path.substring(1);
|
||||
List<String> parts = path.split('/');
|
||||
|
||||
if(parts.isNotEmpty){
|
||||
String path = '';
|
||||
for (String part in parts) {
|
||||
path+='/$part';
|
||||
menus.add(path);
|
||||
}
|
||||
}
|
||||
|
||||
if(state.expandMenus.contains(menu.path)){
|
||||
menus.remove(menu.path);
|
||||
}
|
||||
|
||||
state = state.copyWith(expandMenus: menus);
|
||||
|
||||
}
|
||||
setState(() {
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:iroute/cases/cases.dart';
|
||||
|
||||
import '../color_page.dart';
|
||||
|
||||
// GoRouter configuration
|
||||
final goRouter = GoRouter(
|
||||
initialLocation: '/manager',
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/dashboard',
|
||||
builder: (_,__)=>SizedBox(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'data_analyse',
|
||||
// builder: (context, state) => ColorPage(title: 'data_analyse',),
|
||||
pageBuilder: (context, state) {
|
||||
return CustomTransitionPage(
|
||||
key: state.pageKey,
|
||||
child: ColorPage(title: 'data_analyse',),
|
||||
transitionsBuilder: (context, animation, secondaryAnimation, child) {
|
||||
// Change the opacity of the screen using a Curve based on the the animation's
|
||||
// value
|
||||
return FadeTransition(
|
||||
opacity:
|
||||
CurveTween(curve: Curves.easeInOutCirc).animate(animation),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
GoRoute(
|
||||
path: 'work_board',
|
||||
builder: (_,__)=>SizedBox(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'a',
|
||||
builder: (context, state) => ColorPage(title: '第一工作区',),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'b',
|
||||
builder: (context, state) => ColorPage(title: '第二工作区',),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'c',
|
||||
builder: (context, state) => ColorPage(title: '第三工作区',),
|
||||
),
|
||||
]
|
||||
),
|
||||
|
||||
]
|
||||
),
|
||||
|
||||
GoRoute(
|
||||
path: '/cases',
|
||||
builder: (_,__)=>SizedBox(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'counter',
|
||||
builder: (context, state) => CounterPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'guess',
|
||||
builder: (context, state) => GuessPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'muyu',
|
||||
builder: (context, state) => ColorPage(title: '第三工作区',),
|
||||
), GoRoute(
|
||||
path: 'canvas',
|
||||
builder: (context, state) => Paper(),
|
||||
), GoRoute(
|
||||
path: 'muyu',
|
||||
builder: (context, state) => ColorPage(title: '第三工作区',),
|
||||
),
|
||||
]
|
||||
),
|
||||
GoRoute(
|
||||
path: '/manager', builder: (_,__)=>SizedBox(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'account',
|
||||
builder: (context, state) => ColorPage(title: 'account',),
|
||||
),
|
||||
GoRoute(
|
||||
path: 'role',
|
||||
builder: (context, state) => ColorPage(title: 'role',),
|
||||
),
|
||||
]
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
class RootContent extends StatelessWidget {
|
||||
const RootContent({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
|
||||
return Router.withConfig(config: goRouter);
|
||||
}
|
||||
}
|
||||
|
||||
class RootContentDelegate extends RouterDelegate with ChangeNotifier,PopNavigatorRouterDelegateMixin{
|
||||
|
||||
|
||||
@override
|
||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Navigator(
|
||||
key: navigatorKey,
|
||||
pages: [
|
||||
|
||||
],
|
||||
onPopPage: (route, result) {
|
||||
// appState.selectedBook = null;
|
||||
// notifyListeners();
|
||||
return route.didPop(result);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Future<void> setNewRoutePath(configuration) {
|
||||
// TODO: implement setNewRoutePath
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ColorPage extends StatelessWidget {
|
||||
final String title;
|
||||
const ColorPage({super.key, required this.title});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(body: Center(child: Text(title)));
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
|
||||
|
||||
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
class FadeTransitionsBuilder extends PageTransitionsBuilder {
|
||||
/// Constructs a page transition animation that slides the page up.
|
||||
const FadeTransitionsBuilder();
|
||||
|
||||
@override
|
||||
Widget buildTransitions<T>(
|
||||
PageRoute<T>? route,
|
||||
BuildContext? context,
|
||||
Animation<double> animation,
|
||||
Animation<double>? secondaryAnimation,
|
||||
Widget child,
|
||||
) {
|
||||
return _FadeUpwardsPageTransition(routeAnimation: animation, child: child);
|
||||
}
|
||||
}
|
||||
|
||||
class _FadeUpwardsPageTransition extends StatelessWidget {
|
||||
_FadeUpwardsPageTransition({
|
||||
required Animation<double> routeAnimation, // The route's linear 0.0 - 1.0 animation.
|
||||
required this.child,
|
||||
}) : _positionAnimation = routeAnimation.drive(_bottomUpTween.chain(_fastOutSlowInTween)),
|
||||
_opacityAnimation = routeAnimation.drive(_easeInTween);
|
||||
|
||||
// Fractional offset from 1/4 screen below the top to fully on screen.
|
||||
static final Tween<Offset> _bottomUpTween = Tween<Offset>(
|
||||
begin: const Offset(0.25,0),
|
||||
end: Offset.zero,
|
||||
);
|
||||
static final Animatable<double> _fastOutSlowInTween = CurveTween(curve: Curves.fastOutSlowIn);
|
||||
static final Animatable<double> _easeInTween = CurveTween(curve: Curves.easeIn);
|
||||
|
||||
final Animation<Offset> _positionAnimation;
|
||||
final Animation<double> _opacityAnimation;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// return FadeTransition(
|
||||
// opacity: _opacityAnimation,
|
||||
// child: child,
|
||||
// );
|
||||
|
||||
return SlideTransition(
|
||||
position: _positionAnimation,
|
||||
child: FadeTransition(
|
||||
opacity: _opacityAnimation,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_strategy/url_strategy.dart';
|
||||
|
||||
import 'pages/app.dart';
|
||||
|
||||
|
||||
|
||||
void main() {
|
||||
setHashUrlStrategy();
|
||||
runApp(const App());
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../store/app_state.dart';
|
||||
import '../route/parser.dart';
|
||||
import '../route/route_state.dart';
|
||||
|
||||
class App extends StatefulWidget {
|
||||
const App({super.key});
|
||||
|
||||
@override
|
||||
State<App> createState() => _AppState();
|
||||
}
|
||||
|
||||
class _AppState extends State<App> {
|
||||
late final RootRouterDelegate _delegate;
|
||||
late final AppRouteParser _parser;
|
||||
late final AppState _appState;
|
||||
late final RouteState _routeState;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_parser = AppRouteParser(initialRoute: '/color', allowPaths: [
|
||||
'/color',
|
||||
'/color/add',
|
||||
'/color/detail/:colorHex',
|
||||
]);
|
||||
_appState = AppState.initial();
|
||||
_routeState = RouteState(_parser);
|
||||
_delegate = RootRouterDelegate(_routeState);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppStateScope(
|
||||
notifier: _appState,
|
||||
child: RouteStateScope(
|
||||
notifier: _routeState,
|
||||
child: MaterialApp.router(
|
||||
routerDelegate: _delegate,
|
||||
routeInformationParser: _parser,
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
appBarTheme: const AppBarTheme(
|
||||
elevation: 0,
|
||||
iconTheme: IconThemeData(color: Colors.black),
|
||||
titleTextStyle: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
))),
|
||||
debugShowCheckedModeBanner: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ColorAddPage extends StatefulWidget {
|
||||
const ColorAddPage({super.key});
|
||||
|
||||
@override
|
||||
State<ColorAddPage> createState() => _ColorAddPageState();
|
||||
}
|
||||
|
||||
class _ColorAddPageState extends State<ColorAddPage> {
|
||||
late Color _color;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_color = randomColor;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String text = '# ${_color.value.toRadixString(16)}';
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('添加颜色'),
|
||||
actions: [IconButton(onPressed: _selectColor, icon: Icon(Icons.check ))],
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0,vertical: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Text(text,style: TextStyle(color: _color,fontSize: 24,letterSpacing: 4),)),
|
||||
Container(
|
||||
margin: EdgeInsets.only(left: 10),
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: Icon(
|
||||
Icons.sell_outlined,
|
||||
color: Colors.white,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: _color,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Random _random = Random();
|
||||
|
||||
Color get randomColor {
|
||||
return Color.fromARGB(
|
||||
255,
|
||||
_random.nextInt(256),
|
||||
_random.nextInt(256),
|
||||
_random.nextInt(256),
|
||||
);
|
||||
}
|
||||
|
||||
void _selectColor() {
|
||||
Navigator.of(context).pop(_color);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../store/app_state.dart';
|
||||
import '../route/route_state.dart';
|
||||
import 'color_add_page.dart';
|
||||
|
||||
import '../../../common/components/colors_panel.dart';
|
||||
import '../../../common/pages/stl_color_page.dart';
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
const HomePage({super.key});
|
||||
|
||||
@override
|
||||
State<HomePage> createState() => _HomePageState();
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> {
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
AppState state = AppStateScope.of(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(title:const Text('颜色主页')),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _toAddPage,
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
body: ColorsPanel(
|
||||
colors: state.colors,
|
||||
onSelect: _selectColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _selectColor(Color color){
|
||||
String value = '#${color.value.toRadixString(16)}';
|
||||
RouteStateScope.of(context).update('/color/detail/$value');
|
||||
}
|
||||
|
||||
void _toAddPage() async {
|
||||
RouteStateScope.of(context).update('/color/add');
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:quiver/core.dart';
|
||||
|
||||
/// A route path that has been parsed by [TemplateRouteParser].
|
||||
class ParsedRoute {
|
||||
/// The current path location without query parameters. (/book/123)
|
||||
final String path;
|
||||
|
||||
/// The path template (/book/:id)
|
||||
final String pathTemplate;
|
||||
|
||||
/// The path parameters ({id: 123})
|
||||
final Map<String, String> parameters;
|
||||
|
||||
/// The query parameters ({search: abc})
|
||||
final Map<String, String> queryParameters;
|
||||
|
||||
static const _mapEquality = MapEquality<String, String>();
|
||||
|
||||
ParsedRoute(
|
||||
this.path, this.pathTemplate, this.parameters, this.queryParameters);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is ParsedRoute &&
|
||||
other.pathTemplate == pathTemplate &&
|
||||
other.path == path &&
|
||||
_mapEquality.equals(parameters, other.parameters) &&
|
||||
_mapEquality.equals(queryParameters, other.queryParameters);
|
||||
|
||||
@override
|
||||
int get hashCode => hash4(
|
||||
path,
|
||||
pathTemplate,
|
||||
_mapEquality.hash(parameters),
|
||||
_mapEquality.hash(queryParameters),
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() => '<ParsedRoute '
|
||||
'template: $pathTemplate '
|
||||
'path: $path '
|
||||
'parameters: $parameters '
|
||||
'query parameters: $queryParameters>';
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:iroute/common/pages/stl_color_page.dart';
|
||||
import 'package:path_to_regexp/path_to_regexp.dart';
|
||||
import '../pages/home_page.dart';
|
||||
import '../pages/color_add_page.dart';
|
||||
import '../store/app_state.dart';
|
||||
import 'parsed_route.dart';
|
||||
import 'route_state.dart';
|
||||
|
||||
class AppRouteParser extends RouteInformationParser<ParsedRoute> {
|
||||
final List<String> allowPaths;
|
||||
final ParsedRoute initialRoute;
|
||||
|
||||
AppRouteParser({
|
||||
required this.allowPaths,
|
||||
String initialRoute = '/',
|
||||
}) : initialRoute = ParsedRoute(initialRoute, initialRoute, {}, {});
|
||||
|
||||
@override
|
||||
Future<ParsedRoute> parseRouteInformation(
|
||||
RouteInformation routeInformation) async {
|
||||
print(
|
||||
"=======parseRouteInformation:${routeInformation.uri.path}===================");
|
||||
|
||||
final uri = routeInformation.uri;
|
||||
final path = uri.toString();
|
||||
final queryParams = uri.queryParameters;
|
||||
|
||||
ParsedRoute parsedRoute = initialRoute;
|
||||
|
||||
for (var pathTemplate in allowPaths) {
|
||||
final parameters = <String>[];
|
||||
var pathRegExp = pathToRegExp(pathTemplate, parameters: parameters);
|
||||
if (pathRegExp.hasMatch(path)) {
|
||||
final match = pathRegExp.matchAsPrefix(path);
|
||||
if (match == null) continue;
|
||||
final params = extract(parameters, match);
|
||||
parsedRoute = ParsedRoute(path, pathTemplate, params, queryParams);
|
||||
}
|
||||
}
|
||||
return parsedRoute;
|
||||
}
|
||||
|
||||
@override
|
||||
RouteInformation? restoreRouteInformation(ParsedRoute configuration) {
|
||||
print("=======restoreRouteInformation:${configuration}===================");
|
||||
|
||||
return RouteInformation(uri: Uri.parse(configuration.path));
|
||||
}
|
||||
}
|
||||
|
||||
class RootRouterDelegate extends RouterDelegate<ParsedRoute>
|
||||
with ChangeNotifier, PopNavigatorRouterDelegateMixin {
|
||||
final RouteState routeState;
|
||||
|
||||
RootRouterDelegate(this.routeState) {
|
||||
routeState.addListener(notifyListeners);
|
||||
}
|
||||
|
||||
@override
|
||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
@override
|
||||
ParsedRoute get currentConfiguration {
|
||||
print("=======currentConfiguration:${routeState.route.path}===================");
|
||||
|
||||
return routeState.route;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
routeState.removeListener(notifyListeners);
|
||||
routeState.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
final _addColorKey = const ValueKey('addColor');
|
||||
final _detailColorKey = const ValueKey('detailColor');
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final routeState = RouteStateScope.of(context);
|
||||
List<Page> pages = [];
|
||||
final String pathTemplate = routeState.route.pathTemplate;
|
||||
|
||||
pages.add(MaterialPage(child: HomePage()));
|
||||
|
||||
String? selectedColor;
|
||||
|
||||
if (pathTemplate == '/color/detail/:colorHex') {
|
||||
selectedColor = routeState.route.parameters['colorHex'];
|
||||
}
|
||||
|
||||
if (selectedColor != null) {
|
||||
int value = int.parse(
|
||||
selectedColor.substring(
|
||||
1,
|
||||
),
|
||||
radix: 16);
|
||||
pages.add(MaterialPage(
|
||||
key: _detailColorKey,
|
||||
child: StlColorPage(
|
||||
color: Color(value),
|
||||
)));
|
||||
}
|
||||
|
||||
bool isAddPage = pathTemplate == '/color/add';
|
||||
if (isAddPage) {
|
||||
pages.add(
|
||||
MaterialPage(key: _addColorKey, child: ColorAddPage()),
|
||||
);
|
||||
}
|
||||
print("===build====================");
|
||||
return Navigator(
|
||||
key: navigatorKey,
|
||||
pages: pages,
|
||||
onPopPage: (route, result) {
|
||||
print(result);
|
||||
if (route.settings is Page &&
|
||||
(route.settings as Page).key == _addColorKey) {
|
||||
if(result is Color){
|
||||
AppStateScope.of(context).addColor(result);
|
||||
}
|
||||
routeState.update('/color');
|
||||
}
|
||||
|
||||
if (route.settings is Page &&
|
||||
(route.settings as Page).key == _detailColorKey) {
|
||||
routeState.update('/color');
|
||||
}
|
||||
// appState.selectedBook = null;
|
||||
// notifyListeners();
|
||||
return route.didPop(result);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 格式: /a /b
|
||||
|
||||
@override
|
||||
Future<void> setNewRoutePath(ParsedRoute configuration) async {
|
||||
print("===setNewRoutePath===${configuration}=================");
|
||||
routeState.route = configuration;
|
||||
return;
|
||||
}
|
||||
|
||||
// @override
|
||||
// Future<void> setInitialRoutePath( configuration) async{
|
||||
// _pages = [_pageMap['/']!];
|
||||
// notifyListeners();
|
||||
// }
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'parsed_route.dart';
|
||||
import 'parser.dart';
|
||||
|
||||
/// The current route state. To change the current route, call obtain the state
|
||||
/// using `RouteStateScope.of(context)` and call `go()`:
|
||||
///
|
||||
/// ```
|
||||
/// RouteStateScope.of(context).go('/book/2');
|
||||
/// ```
|
||||
class RouteState extends ChangeNotifier {
|
||||
final AppRouteParser _parser;
|
||||
ParsedRoute _route;
|
||||
|
||||
RouteState(this._parser) : _route = _parser.initialRoute;
|
||||
|
||||
ParsedRoute get route => _route;
|
||||
|
||||
set route(ParsedRoute route) {
|
||||
// Don't notify listeners if the path hasn't changed.
|
||||
if (_route == route) return;
|
||||
|
||||
_route = route;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> update(String route) async {
|
||||
this.route = await _parser
|
||||
.parseRouteInformation(RouteInformation(uri: Uri.parse(route)));
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides the current [RouteState] to descendant widgets in the tree.
|
||||
class RouteStateScope extends InheritedNotifier<RouteState> {
|
||||
const RouteStateScope({
|
||||
required super.notifier,
|
||||
required super.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
static RouteState of(BuildContext context) =>
|
||||
context.dependOnInheritedWidgetOfExactType<RouteStateScope>()!.notifier!;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppState extends ChangeNotifier{
|
||||
final List<Color> colors;
|
||||
|
||||
AppState({required this.colors,});
|
||||
|
||||
AppState.initial():colors=[
|
||||
Colors.red, Colors.black, Colors.blue, Colors.green, Colors.orange,
|
||||
Colors.pink, Colors.purple, Colors.indigo, Colors.amber, Colors.cyan,
|
||||
Colors.redAccent, Colors.grey, Colors.blueAccent, Colors.greenAccent, Colors.orangeAccent,
|
||||
Colors.pinkAccent, Colors.purpleAccent, Colors.indigoAccent, Colors.amberAccent, Colors.cyanAccent,
|
||||
];
|
||||
|
||||
void addColor(Color color){
|
||||
colors.add(color);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Provides the current [RouteState] to descendant widgets in the tree.
|
||||
class AppStateScope extends InheritedNotifier<AppState> {
|
||||
const AppStateScope({super.key, required super.child, required super.notifier,});
|
||||
|
||||
|
||||
|
||||
static AppState of(BuildContext context) =>
|
||||
context.dependOnInheritedWidgetOfExactType<AppStateScope>()!.notifier!;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'pages/app/app.dart';
|
||||
|
||||
|
||||
|
||||
void main() {
|
||||
runApp(const App());
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import 'package:toly_menu/toly_menu.dart';
|
||||
|
||||
List<MenuData> menus = [
|
||||
MenuData(path: '/dashboard', label: '总览面板', children: [
|
||||
MenuData(path: '/dashboard/data_analyse', label: '数据分析', deep: 1),
|
||||
MenuData(path: '/dashboard/work_board', label: '工作台', deep: 1, children: [
|
||||
MenuData(path: '/dashboard/work_board/a', label: '第一工作区', deep: 2),
|
||||
MenuData(path: '/dashboard/work_board/b', label: '第二工作区', deep: 2),
|
||||
MenuData(path: '/dashboard/work_board/c', label: '第三工作区', deep: 2),
|
||||
]),
|
||||
]),
|
||||
MenuData(path: '/manager', label: '系统管理', children: [
|
||||
MenuData(path: '/manager/account', label: '账号管理', deep: 1),
|
||||
MenuData(path: '/manager/role', label: '角色管理', deep: 1),
|
||||
MenuData(path: '/manager/menu', label: '菜单管理', deep: 1),
|
||||
]),
|
||||
MenuData(path: '/knowledge', label: '知识库管理', children: [
|
||||
MenuData(path: '/knowledge/a', label: '语文知识', deep: 1, children: [
|
||||
MenuData(
|
||||
path: '/knowledge/a/1',
|
||||
label: '诗词歌赋',
|
||||
deep: 2,
|
||||
),
|
||||
MenuData(
|
||||
path: '/knowledge/a/2',
|
||||
label: '人物故事',
|
||||
deep: 2,
|
||||
),
|
||||
MenuData(
|
||||
path: '/knowledge/a/3',
|
||||
label: '名著推荐',
|
||||
deep: 2,
|
||||
)
|
||||
]),
|
||||
MenuData(
|
||||
path: '/knowledge/b',
|
||||
label: '数学知识',
|
||||
children: [
|
||||
MenuData(
|
||||
path: '/knowledge/b/1',
|
||||
label: '人物故事',
|
||||
deep: 2,
|
||||
),
|
||||
MenuData(
|
||||
path: '/knowledge/b/2',
|
||||
label: '数学定理',
|
||||
deep: 2,
|
||||
),
|
||||
MenuData(
|
||||
path: '/knowledge/b/3',
|
||||
label: '几何知识',
|
||||
deep: 2,
|
||||
),
|
||||
MenuData(
|
||||
path: '/knowledge/b/4',
|
||||
label: '代数知识',
|
||||
deep: 2,
|
||||
)
|
||||
],
|
||||
deep: 1),
|
||||
MenuData(
|
||||
path: '/knowledge/c',
|
||||
label: '英语知识',
|
||||
deep: 1,
|
||||
),
|
||||
]),
|
||||
MenuData(path: '/widgets', label: '组件集录', children: [
|
||||
MenuData(path: '/widgets/stateless', label: '无状态组件', deep: 1),
|
||||
MenuData(path: '/widgets/stateful', label: '有状态组件', deep: 1),
|
||||
MenuData(path: '/widgets/single_child', label: '单子渲染组件', deep: 1),
|
||||
MenuData(path: '/widgets/mutli_child', label: '多子渲染组件', deep: 1),
|
||||
MenuData(path: '/widgets/sliver', label: '滑片组件', deep: 1),
|
||||
MenuData(path: '/widgets/proxy', label: '代理组件', deep: 1),
|
||||
MenuData(path: '/widgets/other', label: '其他组件', deep: 1),
|
||||
])
|
||||
];
|
||||
@@ -1,89 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:toly_menu/toly_menu.dart';
|
||||
|
||||
import '../../menus.dart';
|
||||
|
||||
class App extends StatelessWidget {
|
||||
const App({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
theme: ThemeData(
|
||||
appBarTheme: const AppBarTheme(
|
||||
elevation: 0,
|
||||
iconTheme: IconThemeData(color: Colors.black),
|
||||
titleTextStyle: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
))),
|
||||
debugShowCheckedModeBanner: false,
|
||||
home: AppLayout());
|
||||
}
|
||||
}
|
||||
|
||||
class AppLayout extends StatelessWidget {
|
||||
const AppLayout({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 210,
|
||||
child: MenuPanel(),
|
||||
),
|
||||
// Container()
|
||||
],
|
||||
),
|
||||
// appBar: AppToolBar(),
|
||||
// body: AppNavigation(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MenuPanel extends StatefulWidget {
|
||||
const MenuPanel({super.key});
|
||||
|
||||
@override
|
||||
State<MenuPanel> createState() => _MenuPanelState();
|
||||
}
|
||||
|
||||
class _MenuPanelState extends State<MenuPanel> {
|
||||
MenuState state = MenuState(expandMenus: ['/dashboard'], activeMenu: '/dashboard/data_analyse', items: menus);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TolyMenu(state: state, onSelect: _onSelect);
|
||||
}
|
||||
|
||||
void _onSelect(MenuData menu) {
|
||||
if(menu.isLeaf){
|
||||
state = state.copyWith(activeMenu: menu.path);
|
||||
}else{
|
||||
List<String> menus = [];
|
||||
String path = menu.path.substring(1);
|
||||
List<String> parts = path.split('/');
|
||||
|
||||
if(parts.isNotEmpty){
|
||||
String path = '';
|
||||
for (String part in parts) {
|
||||
path+='/$part';
|
||||
menus.add(path);
|
||||
}
|
||||
}
|
||||
|
||||
if(state.expandMenus.contains(menu.path)){
|
||||
menus.remove(menu.path);
|
||||
}
|
||||
|
||||
state = state.copyWith(expandMenus: menus);
|
||||
|
||||
}
|
||||
setState(() {
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class RootContent extends StatelessWidget {
|
||||
const RootContent({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Router(routerDelegate: RootContentDelegate());
|
||||
}
|
||||
}
|
||||
|
||||
class RootContentDelegate extends RouterDelegate with ChangeNotifier,PopNavigatorRouterDelegateMixin{
|
||||
|
||||
|
||||
@override
|
||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Navigator(
|
||||
key: navigatorKey,
|
||||
pages: [
|
||||
|
||||
],
|
||||
onPopPage: (route, result) {
|
||||
// appState.selectedBook = null;
|
||||
// notifyListeners();
|
||||
return route.didPop(result);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Future<void> setNewRoutePath(configuration) {
|
||||
// TODO: implement setNewRoutePath
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -1,244 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
// This scenario demonstrates how to use redirect to handle a asynchronous
|
||||
// sign-in flow.
|
||||
//
|
||||
// The `StreamAuth` is a mock of google_sign_in. This example wraps it with an
|
||||
// InheritedNotifier, StreamAuthScope, and relies on
|
||||
// `dependOnInheritedWidgetOfExactType` to create a dependency between the
|
||||
// notifier and go_router's parsing pipeline. When StreamAuth broadcasts new
|
||||
// event, the dependency will cause the go_router to reparse the current url
|
||||
// which will also trigger the redirect.
|
||||
|
||||
void main() => runApp(StreamAuthScope(child: App()));
|
||||
|
||||
/// The main app.
|
||||
class App extends StatelessWidget {
|
||||
/// Creates an [App].
|
||||
App({super.key});
|
||||
|
||||
/// The title of the app.
|
||||
static const String title = 'GoRouter Example: Redirection';
|
||||
|
||||
// add the login info into the tree as app state that can change over time
|
||||
@override
|
||||
Widget build(BuildContext context) => MaterialApp.router(
|
||||
routerConfig: _router,
|
||||
title: title,
|
||||
debugShowCheckedModeBanner: false,
|
||||
);
|
||||
|
||||
late final GoRouter _router = GoRouter(
|
||||
routes: <GoRoute>[
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (BuildContext context, GoRouterState state) =>
|
||||
const HomeScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/login',
|
||||
builder: (BuildContext context, GoRouterState state) =>
|
||||
const LoginScreen(),
|
||||
),
|
||||
],
|
||||
|
||||
// redirect to the login page if the user is not logged in
|
||||
redirect: (BuildContext context, GoRouterState state) async {
|
||||
// Using `of` method creates a dependency of StreamAuthScope. It will
|
||||
// cause go_router to reparse current route if StreamAuth has new sign-in
|
||||
// information.
|
||||
final bool loggedIn = await StreamAuthScope.of(context).isSignedIn();
|
||||
final bool loggingIn = state.matchedLocation == '/login';
|
||||
if (!loggedIn) {
|
||||
return '/login';
|
||||
}
|
||||
|
||||
// if the user is logged in but still on the login page, send them to
|
||||
// the home page
|
||||
if (loggingIn) {
|
||||
return '/';
|
||||
}
|
||||
|
||||
// no need to redirect at all
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// The login screen.
|
||||
class LoginScreen extends StatefulWidget {
|
||||
/// Creates a [LoginScreen].
|
||||
const LoginScreen({super.key});
|
||||
|
||||
@override
|
||||
State<LoginScreen> createState() => _LoginScreenState();
|
||||
}
|
||||
|
||||
class _LoginScreenState extends State<LoginScreen>
|
||||
with TickerProviderStateMixin {
|
||||
bool loggingIn = false;
|
||||
late final AnimationController controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(seconds: 1),
|
||||
)..addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
controller.repeat();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(title: const Text(App.title)),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
if (loggingIn) CircularProgressIndicator(value: controller.value),
|
||||
if (!loggingIn)
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
StreamAuthScope.of(context).signIn('test-user');
|
||||
setState(() {
|
||||
loggingIn = true;
|
||||
});
|
||||
},
|
||||
child: const Text('Login'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// The home screen.
|
||||
class HomeScreen extends StatelessWidget {
|
||||
/// Creates a [HomeScreen].
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final StreamAuth info = StreamAuthScope.of(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(App.title),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
onPressed: () => info.signOut(),
|
||||
tooltip: 'Logout: ${info.currentUser}',
|
||||
icon: const Icon(Icons.logout),
|
||||
)
|
||||
],
|
||||
),
|
||||
body: const Center(
|
||||
child: Text('HomeScreen'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A scope that provides [StreamAuth] for the subtree.
|
||||
class StreamAuthScope extends InheritedNotifier<StreamAuthNotifier> {
|
||||
/// Creates a [StreamAuthScope] sign in scope.
|
||||
StreamAuthScope({
|
||||
super.key,
|
||||
required super.child,
|
||||
}) : super(
|
||||
notifier: StreamAuthNotifier(),
|
||||
);
|
||||
|
||||
/// Gets the [StreamAuth].
|
||||
static StreamAuth of(BuildContext context) {
|
||||
return context
|
||||
.dependOnInheritedWidgetOfExactType<StreamAuthScope>()!
|
||||
.notifier!
|
||||
.streamAuth;
|
||||
}
|
||||
}
|
||||
|
||||
/// A class that converts [StreamAuth] into a [ChangeNotifier].
|
||||
class StreamAuthNotifier extends ChangeNotifier {
|
||||
/// Creates a [StreamAuthNotifier].
|
||||
StreamAuthNotifier() : streamAuth = StreamAuth() {
|
||||
streamAuth.onCurrentUserChanged.listen((String? string) {
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
/// The stream auth client.
|
||||
final StreamAuth streamAuth;
|
||||
}
|
||||
|
||||
/// An asynchronous log in services mock with stream similar to google_sign_in.
|
||||
///
|
||||
/// This class adds an artificial delay of 3 second when logging in an user, and
|
||||
/// will automatically clear the login session after [refreshInterval].
|
||||
class StreamAuth {
|
||||
/// Creates an [StreamAuth] that clear the current user session in
|
||||
/// [refeshInterval] second.
|
||||
StreamAuth({this.refreshInterval = 20})
|
||||
: _userStreamController = StreamController<String?>.broadcast() {
|
||||
_userStreamController.stream.listen((String? currentUser) {
|
||||
_currentUser = currentUser;
|
||||
});
|
||||
}
|
||||
|
||||
/// The current user.
|
||||
String? get currentUser => _currentUser;
|
||||
String? _currentUser;
|
||||
|
||||
/// Checks whether current user is signed in with an artificial delay to mimic
|
||||
/// async operation.
|
||||
Future<bool> isSignedIn() async {
|
||||
await Future<void>.delayed(const Duration(seconds: 1));
|
||||
return _currentUser != null;
|
||||
}
|
||||
|
||||
/// A stream that notifies when current user has changed.
|
||||
Stream<String?> get onCurrentUserChanged => _userStreamController.stream;
|
||||
final StreamController<String?> _userStreamController;
|
||||
|
||||
/// The interval that automatically signs out the user.
|
||||
final int refreshInterval;
|
||||
|
||||
Timer? _timer;
|
||||
Timer _createRefreshTimer() {
|
||||
return Timer(Duration(seconds: refreshInterval), () {
|
||||
_userStreamController.add(null);
|
||||
_timer = null;
|
||||
});
|
||||
}
|
||||
|
||||
/// Signs in a user with an artificial delay to mimic async operation.
|
||||
Future<void> signIn(String newUserName) async {
|
||||
await Future<void>.delayed(const Duration(seconds: 3));
|
||||
_userStreamController.add(newUserName);
|
||||
_timer?.cancel();
|
||||
_timer = _createRefreshTimer();
|
||||
}
|
||||
|
||||
/// Signs out the current user.
|
||||
Future<void> signOut() async {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
_userStreamController.add(null);
|
||||
}
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'src/auth.dart';
|
||||
import 'src/data/author.dart';
|
||||
import 'src/data/book.dart';
|
||||
import 'src/data/library.dart';
|
||||
import 'src/screens/author_details.dart';
|
||||
import 'src/screens/authors.dart';
|
||||
import 'src/screens/book_details.dart';
|
||||
import 'src/screens/books.dart';
|
||||
import 'src/screens/scaffold.dart';
|
||||
import 'src/screens/settings.dart';
|
||||
import 'src/screens/sign_in.dart';
|
||||
|
||||
void main() => runApp(Bookstore());
|
||||
|
||||
/// The book store view.
|
||||
class Bookstore extends StatelessWidget {
|
||||
/// Creates a [Bookstore].
|
||||
Bookstore({super.key});
|
||||
|
||||
final ValueKey<String> _scaffoldKey = const ValueKey<String>('App scaffold');
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => BookstoreAuthScope(
|
||||
notifier: _auth,
|
||||
child: MaterialApp.router(
|
||||
routerConfig: _router,
|
||||
),
|
||||
);
|
||||
|
||||
final BookstoreAuth _auth = BookstoreAuth();
|
||||
|
||||
late final GoRouter _router = GoRouter(
|
||||
routes: <GoRoute>[
|
||||
GoRoute(
|
||||
path: '/',
|
||||
redirect: (_, __) => '/books',
|
||||
),
|
||||
GoRoute(
|
||||
path: '/signin',
|
||||
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||
FadeTransitionPage(
|
||||
key: state.pageKey,
|
||||
child: SignInScreen(
|
||||
onSignIn: (Credentials credentials) {
|
||||
BookstoreAuthScope.of(context)
|
||||
.signIn(credentials.username, credentials.password);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/books',
|
||||
redirect: (_, __) => '/books/popular',
|
||||
),
|
||||
GoRoute(
|
||||
path: '/book/:bookId',
|
||||
redirect: (BuildContext context, GoRouterState state) =>
|
||||
'/books/all/${state.pathParameters['bookId']}',
|
||||
),
|
||||
GoRoute(
|
||||
path: '/books/:kind(new|all|popular)',
|
||||
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||
FadeTransitionPage(
|
||||
key: _scaffoldKey,
|
||||
child: BookstoreScaffold(
|
||||
selectedTab: ScaffoldTab.books,
|
||||
child: BooksScreen(state.pathParameters['kind']!),
|
||||
),
|
||||
),
|
||||
routes: <GoRoute>[
|
||||
GoRoute(
|
||||
path: ':bookId',
|
||||
builder: (BuildContext context, GoRouterState state) {
|
||||
final String bookId = state.pathParameters['bookId']!;
|
||||
final Book? selectedBook = libraryInstance.allBooks
|
||||
.firstWhereOrNull((Book b) => b.id.toString() == bookId);
|
||||
|
||||
return BookDetailsScreen(book: selectedBook);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/author/:authorId',
|
||||
redirect: (BuildContext context, GoRouterState state) =>
|
||||
'/authors/${state.pathParameters['authorId']}',
|
||||
),
|
||||
GoRoute(
|
||||
path: '/authors',
|
||||
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||
FadeTransitionPage(
|
||||
key: _scaffoldKey,
|
||||
child: const BookstoreScaffold(
|
||||
selectedTab: ScaffoldTab.authors,
|
||||
child: AuthorsScreen(),
|
||||
),
|
||||
),
|
||||
routes: <GoRoute>[
|
||||
GoRoute(
|
||||
path: ':authorId',
|
||||
builder: (BuildContext context, GoRouterState state) {
|
||||
final int authorId = int.parse(state.pathParameters['authorId']!);
|
||||
final Author? selectedAuthor = libraryInstance.allAuthors
|
||||
.firstWhereOrNull((Author a) => a.id == authorId);
|
||||
|
||||
return AuthorDetailsScreen(author: selectedAuthor);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/settings',
|
||||
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||
FadeTransitionPage(
|
||||
key: _scaffoldKey,
|
||||
child: const BookstoreScaffold(
|
||||
selectedTab: ScaffoldTab.settings,
|
||||
child: SettingsScreen(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
redirect: _guard,
|
||||
refreshListenable: _auth,
|
||||
debugLogDiagnostics: true,
|
||||
);
|
||||
|
||||
String? _guard(BuildContext context, GoRouterState state) {
|
||||
final bool signedIn = _auth.signedIn;
|
||||
final bool signingIn = state.matchedLocation == '/signin';
|
||||
|
||||
// Go to /signin if the user is not signed in
|
||||
if (!signedIn && !signingIn) {
|
||||
return '/signin';
|
||||
}
|
||||
// Go to /books if the user is signed in and tries to go to /signin.
|
||||
else if (signedIn && signingIn) {
|
||||
return '/books';
|
||||
}
|
||||
|
||||
// no redirect
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// A page that fades in an out.
|
||||
class FadeTransitionPage extends CustomTransitionPage<void> {
|
||||
/// Creates a [FadeTransitionPage].
|
||||
FadeTransitionPage({
|
||||
required LocalKey super.key,
|
||||
required super.child,
|
||||
}) : super(
|
||||
transitionsBuilder: (BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
Widget child) =>
|
||||
FadeTransition(
|
||||
opacity: animation.drive(_curveTween),
|
||||
child: child,
|
||||
));
|
||||
|
||||
static final CurveTween _curveTween = CurveTween(curve: Curves.easeIn);
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// A mock authentication service.
|
||||
class BookstoreAuth extends ChangeNotifier {
|
||||
bool _signedIn = false;
|
||||
|
||||
/// Whether user has signed in.
|
||||
bool get signedIn => _signedIn;
|
||||
|
||||
/// Signs out the current user.
|
||||
Future<void> signOut() async {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 200));
|
||||
// Sign out.
|
||||
_signedIn = false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Signs in a user.
|
||||
Future<bool> signIn(String username, String password) async {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 200));
|
||||
|
||||
// Sign in. Allow any password.
|
||||
_signedIn = true;
|
||||
notifyListeners();
|
||||
return _signedIn;
|
||||
}
|
||||
}
|
||||
|
||||
/// An inherited notifier to host [BookstoreAuth] for the subtree.
|
||||
class BookstoreAuthScope extends InheritedNotifier<BookstoreAuth> {
|
||||
/// Creates a [BookstoreAuthScope].
|
||||
const BookstoreAuthScope({
|
||||
required BookstoreAuth super.notifier,
|
||||
required super.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// Gets the [BookstoreAuth] above the context.
|
||||
static BookstoreAuth of(BuildContext context) => context
|
||||
.dependOnInheritedWidgetOfExactType<BookstoreAuthScope>()!
|
||||
.notifier!;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
export 'data/author.dart';
|
||||
export 'data/book.dart';
|
||||
export 'data/library.dart';
|
||||
@@ -1,23 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'book.dart';
|
||||
|
||||
/// Author data class.
|
||||
class Author {
|
||||
/// Creates an author data object.
|
||||
Author({
|
||||
required this.id,
|
||||
required this.name,
|
||||
});
|
||||
|
||||
/// The id of the author.
|
||||
final int id;
|
||||
|
||||
/// The name of the author.
|
||||
final String name;
|
||||
|
||||
/// The books of the author.
|
||||
final List<Book> books = <Book>[];
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'author.dart';
|
||||
|
||||
/// Book data class.
|
||||
class Book {
|
||||
/// Creates a book data object.
|
||||
Book({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.isPopular,
|
||||
required this.isNew,
|
||||
required this.author,
|
||||
});
|
||||
|
||||
/// The id of the book.
|
||||
final int id;
|
||||
|
||||
/// The title of the book.
|
||||
final String title;
|
||||
|
||||
/// The author of the book.
|
||||
final Author author;
|
||||
|
||||
/// Whether the book is popular.
|
||||
final bool isPopular;
|
||||
|
||||
/// Whether the book is new.
|
||||
final bool isNew;
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'author.dart';
|
||||
import 'book.dart';
|
||||
|
||||
/// Library data mock.
|
||||
final Library libraryInstance = Library()
|
||||
..addBook(
|
||||
title: 'Left Hand of Darkness',
|
||||
authorName: 'Ursula K. Le Guin',
|
||||
isPopular: true,
|
||||
isNew: true)
|
||||
..addBook(
|
||||
title: 'Too Like the Lightning',
|
||||
authorName: 'Ada Palmer',
|
||||
isPopular: false,
|
||||
isNew: true)
|
||||
..addBook(
|
||||
title: 'Kindred',
|
||||
authorName: 'Octavia E. Butler',
|
||||
isPopular: true,
|
||||
isNew: false)
|
||||
..addBook(
|
||||
title: 'The Lathe of Heaven',
|
||||
authorName: 'Ursula K. Le Guin',
|
||||
isPopular: false,
|
||||
isNew: false);
|
||||
|
||||
/// A library that contains books and authors.
|
||||
class Library {
|
||||
/// The books in the library.
|
||||
final List<Book> allBooks = <Book>[];
|
||||
|
||||
/// The authors in the library.
|
||||
final List<Author> allAuthors = <Author>[];
|
||||
|
||||
/// Adds a book into the library.
|
||||
void addBook({
|
||||
required String title,
|
||||
required String authorName,
|
||||
required bool isPopular,
|
||||
required bool isNew,
|
||||
}) {
|
||||
final Author author = allAuthors.firstWhere(
|
||||
(Author author) => author.name == authorName,
|
||||
orElse: () {
|
||||
final Author value = Author(id: allAuthors.length, name: authorName);
|
||||
allAuthors.add(value);
|
||||
return value;
|
||||
},
|
||||
);
|
||||
|
||||
final Book book = Book(
|
||||
id: allBooks.length,
|
||||
title: title,
|
||||
isPopular: isPopular,
|
||||
isNew: isNew,
|
||||
author: author,
|
||||
);
|
||||
|
||||
author.books.add(book);
|
||||
allBooks.add(book);
|
||||
}
|
||||
|
||||
/// The list of popular books in the library.
|
||||
List<Book> get popularBooks => <Book>[
|
||||
...allBooks.where((Book book) => book.isPopular),
|
||||
];
|
||||
|
||||
/// The list of new books in the library.
|
||||
List<Book> get newBooks => <Book>[
|
||||
...allBooks.where((Book book) => book.isNew),
|
||||
];
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../data.dart';
|
||||
import '../widgets/book_list.dart';
|
||||
|
||||
/// The author detail screen.
|
||||
class AuthorDetailsScreen extends StatelessWidget {
|
||||
/// Creates an author detail screen.
|
||||
const AuthorDetailsScreen({
|
||||
required this.author,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// The author to be displayed.
|
||||
final Author? author;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (author == null) {
|
||||
return const Scaffold(
|
||||
body: Center(
|
||||
child: Text('No author found.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(author!.name),
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: BookList(
|
||||
books: author!.books,
|
||||
onTap: (Book book) => context.go('/book/${book.id}'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../data.dart';
|
||||
import '../widgets/author_list.dart';
|
||||
|
||||
/// A screen that displays a list of authors.
|
||||
class AuthorsScreen extends StatelessWidget {
|
||||
/// Creates an [AuthorsScreen].
|
||||
const AuthorsScreen({super.key});
|
||||
|
||||
/// The title of the screen.
|
||||
static const String title = 'Authors';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(title),
|
||||
),
|
||||
body: AuthorList(
|
||||
authors: libraryInstance.allAuthors,
|
||||
onTap: (Author author) {
|
||||
context.go('/author/${author.id}');
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:url_launcher/link.dart';
|
||||
|
||||
import '../data.dart';
|
||||
import 'author_details.dart';
|
||||
|
||||
/// A screen to display book details.
|
||||
class BookDetailsScreen extends StatelessWidget {
|
||||
/// Creates a [BookDetailsScreen].
|
||||
const BookDetailsScreen({
|
||||
super.key,
|
||||
this.book,
|
||||
});
|
||||
|
||||
/// The book to be displayed.
|
||||
final Book? book;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (book == null) {
|
||||
return const Scaffold(
|
||||
body: Center(
|
||||
child: Text('No book found.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(book!.title),
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
book!.title,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
Text(
|
||||
book!.author.name,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).push<void>(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (BuildContext context) =>
|
||||
AuthorDetailsScreen(author: book!.author),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('View author (navigator.push)'),
|
||||
),
|
||||
Link(
|
||||
uri: Uri.parse('/author/${book!.author.id}'),
|
||||
builder: (BuildContext context, FollowLink? followLink) =>
|
||||
TextButton(
|
||||
onPressed: followLink,
|
||||
child: const Text('View author (Link)'),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.push('/author/${book!.author.id}');
|
||||
},
|
||||
child: const Text('View author (GoRouter.push)'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../data.dart';
|
||||
import '../widgets/book_list.dart';
|
||||
|
||||
/// A screen that displays a list of books.
|
||||
class BooksScreen extends StatefulWidget {
|
||||
/// Creates a [BooksScreen].
|
||||
const BooksScreen(this.kind, {super.key});
|
||||
|
||||
/// Which tab to display.
|
||||
final String kind;
|
||||
|
||||
@override
|
||||
State<BooksScreen> createState() => _BooksScreenState();
|
||||
}
|
||||
|
||||
class _BooksScreenState extends State<BooksScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 3, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(BooksScreen oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
switch (widget.kind) {
|
||||
case 'popular':
|
||||
_tabController.index = 0;
|
||||
break;
|
||||
|
||||
case 'new':
|
||||
_tabController.index = 1;
|
||||
break;
|
||||
|
||||
case 'all':
|
||||
_tabController.index = 2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Books'),
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
onTap: _handleTabTapped,
|
||||
tabs: const <Tab>[
|
||||
Tab(
|
||||
text: 'Popular',
|
||||
icon: Icon(Icons.people),
|
||||
),
|
||||
Tab(
|
||||
text: 'New',
|
||||
icon: Icon(Icons.new_releases),
|
||||
),
|
||||
Tab(
|
||||
text: 'All',
|
||||
icon: Icon(Icons.list),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: TabBarView(
|
||||
controller: _tabController,
|
||||
children: <Widget>[
|
||||
BookList(
|
||||
books: libraryInstance.popularBooks,
|
||||
onTap: _handleBookTapped,
|
||||
),
|
||||
BookList(
|
||||
books: libraryInstance.newBooks,
|
||||
onTap: _handleBookTapped,
|
||||
),
|
||||
BookList(
|
||||
books: libraryInstance.allBooks,
|
||||
onTap: _handleBookTapped,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
void _handleBookTapped(Book book) {
|
||||
context.go('/book/${book.id}');
|
||||
}
|
||||
|
||||
void _handleTabTapped(int index) {
|
||||
switch (index) {
|
||||
case 1:
|
||||
context.go('/books/new');
|
||||
break;
|
||||
case 2:
|
||||
context.go('/books/all');
|
||||
break;
|
||||
case 0:
|
||||
default:
|
||||
context.go('/books/popular');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:adaptive_navigation/adaptive_navigation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
/// The enum for scaffold tab.
|
||||
enum ScaffoldTab {
|
||||
/// The books tab.
|
||||
books,
|
||||
|
||||
/// The authors tab.
|
||||
authors,
|
||||
|
||||
/// The settings tab.
|
||||
settings
|
||||
}
|
||||
|
||||
/// The scaffold for the book store.
|
||||
class BookstoreScaffold extends StatelessWidget {
|
||||
/// Creates a [BookstoreScaffold].
|
||||
const BookstoreScaffold({
|
||||
required this.selectedTab,
|
||||
required this.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// Which tab of the scaffold to display.
|
||||
final ScaffoldTab selectedTab;
|
||||
|
||||
/// The scaffold body.
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
body: AdaptiveNavigationScaffold(
|
||||
selectedIndex: selectedTab.index,
|
||||
body: child,
|
||||
onDestinationSelected: (int idx) {
|
||||
switch (ScaffoldTab.values[idx]) {
|
||||
case ScaffoldTab.books:
|
||||
context.go('/books');
|
||||
break;
|
||||
case ScaffoldTab.authors:
|
||||
context.go('/authors');
|
||||
break;
|
||||
case ScaffoldTab.settings:
|
||||
context.go('/settings');
|
||||
break;
|
||||
}
|
||||
},
|
||||
destinations: const <AdaptiveScaffoldDestination>[
|
||||
AdaptiveScaffoldDestination(
|
||||
title: 'Books',
|
||||
icon: Icons.book,
|
||||
),
|
||||
AdaptiveScaffoldDestination(
|
||||
title: 'Authors',
|
||||
icon: Icons.person,
|
||||
),
|
||||
AdaptiveScaffoldDestination(
|
||||
title: 'Settings',
|
||||
icon: Icons.settings,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:url_launcher/link.dart';
|
||||
|
||||
import '../auth.dart';
|
||||
|
||||
/// The settings screen.
|
||||
class SettingsScreen extends StatefulWidget {
|
||||
/// Creates a [SettingsScreen].
|
||||
const SettingsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<SettingsScreen> createState() => _SettingsScreenState();
|
||||
}
|
||||
|
||||
class _SettingsScreenState extends State<SettingsScreen> {
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
child: const Card(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 18, horizontal: 12),
|
||||
child: SettingsContent(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// The content of a [SettingsScreen].
|
||||
class SettingsContent extends StatelessWidget {
|
||||
/// Creates a [SettingsContent].
|
||||
const SettingsContent({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Column(
|
||||
children: <Widget>[
|
||||
...<Widget>[
|
||||
Text(
|
||||
'Settings',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
BookstoreAuthScope.of(context).signOut();
|
||||
},
|
||||
child: const Text('Sign out'),
|
||||
),
|
||||
Link(
|
||||
uri: Uri.parse('/book/0'),
|
||||
builder: (BuildContext context, FollowLink? followLink) =>
|
||||
TextButton(
|
||||
onPressed: followLink,
|
||||
child: const Text('Go directly to /book/0 (Link)'),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.go('/book/0');
|
||||
},
|
||||
child: const Text('Go directly to /book/0 (GoRouter)'),
|
||||
),
|
||||
].map<Widget>((Widget w) =>
|
||||
Padding(padding: const EdgeInsets.all(8), child: w)),
|
||||
TextButton(
|
||||
onPressed: () => showDialog<String>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => AlertDialog(
|
||||
title: const Text('Alert!'),
|
||||
content: const Text('The alert description goes here.'),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, 'Cancel'),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, 'OK'),
|
||||
child: const Text('OK'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: const Text('Show Dialog'),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Credential data class.
|
||||
class Credentials {
|
||||
/// Creates a credential data object.
|
||||
Credentials(this.username, this.password);
|
||||
|
||||
/// The username of the credentials.
|
||||
final String username;
|
||||
|
||||
/// The password of the credentials.
|
||||
final String password;
|
||||
}
|
||||
|
||||
/// The sign-in screen.
|
||||
class SignInScreen extends StatefulWidget {
|
||||
/// Creates a sign-in screen.
|
||||
const SignInScreen({
|
||||
required this.onSignIn,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// Called when users sign in with [Credentials].
|
||||
final ValueChanged<Credentials> onSignIn;
|
||||
|
||||
@override
|
||||
State<SignInScreen> createState() => _SignInScreenState();
|
||||
}
|
||||
|
||||
class _SignInScreenState extends State<SignInScreen> {
|
||||
final TextEditingController _usernameController = TextEditingController();
|
||||
final TextEditingController _passwordController = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
body: Center(
|
||||
child: Card(
|
||||
child: Container(
|
||||
constraints: BoxConstraints.loose(const Size(600, 600)),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text('Sign in',
|
||||
style: Theme.of(context).textTheme.headlineMedium),
|
||||
TextField(
|
||||
decoration: const InputDecoration(labelText: 'Username'),
|
||||
controller: _usernameController,
|
||||
),
|
||||
TextField(
|
||||
decoration: const InputDecoration(labelText: 'Password'),
|
||||
obscureText: true,
|
||||
controller: _passwordController,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: TextButton(
|
||||
onPressed: () async {
|
||||
widget.onSignIn(Credentials(
|
||||
_usernameController.value.text,
|
||||
_passwordController.value.text));
|
||||
},
|
||||
child: const Text('Sign in'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../data.dart';
|
||||
|
||||
/// The author list view.
|
||||
class AuthorList extends StatelessWidget {
|
||||
/// Creates an [AuthorList].
|
||||
const AuthorList({
|
||||
required this.authors,
|
||||
this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// The list of authors to be shown.
|
||||
final List<Author> authors;
|
||||
|
||||
/// Called when the user taps an author.
|
||||
final ValueChanged<Author>? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ListView.builder(
|
||||
itemCount: authors.length,
|
||||
itemBuilder: (BuildContext context, int index) => ListTile(
|
||||
title: Text(
|
||||
authors[index].name,
|
||||
),
|
||||
subtitle: Text(
|
||||
'${authors[index].books.length} books',
|
||||
),
|
||||
onTap: onTap != null ? () => onTap!(authors[index]) : null,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../data.dart';
|
||||
|
||||
/// The book list view.
|
||||
class BookList extends StatelessWidget {
|
||||
/// Creates an [BookList].
|
||||
const BookList({
|
||||
required this.books,
|
||||
this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// The list of books to be displayed.
|
||||
final List<Book> books;
|
||||
|
||||
/// Called when the user taps a book.
|
||||
final ValueChanged<Book>? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ListView.builder(
|
||||
itemCount: books.length,
|
||||
itemBuilder: (BuildContext context, int index) => ListTile(
|
||||
title: Text(
|
||||
books[index].title,
|
||||
),
|
||||
subtitle: Text(
|
||||
books[index].author.name,
|
||||
),
|
||||
onTap: onTap != null ? () => onTap!(books[index]) : null,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
/// This sample app shows how to use `GoRouter.onException` to redirect on
|
||||
/// exception.
|
||||
///
|
||||
/// The first route '/' is mapped to [HomeScreen], and the second route
|
||||
/// '/404' is mapped to [NotFoundScreen].
|
||||
///
|
||||
/// Any other unknown route or exception is redirected to `/404`.
|
||||
void main() => runApp(const MyApp());
|
||||
|
||||
/// The route configuration.
|
||||
final GoRouter _router = GoRouter(
|
||||
onException: (_, GoRouterState state, GoRouter router) {
|
||||
router.go('/404', extra: state.uri.toString());
|
||||
},
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (BuildContext context, GoRouterState state) {
|
||||
return const HomeScreen();
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/404',
|
||||
builder: (BuildContext context, GoRouterState state) {
|
||||
return NotFoundScreen(uri: state.extra as String? ?? '');
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
/// The main app.
|
||||
class MyApp extends StatelessWidget {
|
||||
/// Constructs a [MyApp]
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp.router(
|
||||
routerConfig: _router,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The home screen
|
||||
class HomeScreen extends StatelessWidget {
|
||||
/// Constructs a [HomeScreen]
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Home Screen')),
|
||||
body: Center(
|
||||
child: ElevatedButton(
|
||||
onPressed: () => context.go('/some-unknown-route'),
|
||||
child: const Text('Simulates user entering unknown url'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The not found screen
|
||||
class NotFoundScreen extends StatelessWidget {
|
||||
/// Constructs a [HomeScreen]
|
||||
const NotFoundScreen({super.key, required this.uri});
|
||||
|
||||
/// The uri that can not be found.
|
||||
final String uri;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Page Not Found')),
|
||||
body: Center(
|
||||
child: Text("Can't find a page for: $uri"),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
/// This sample app shows an app with two screens.
|
||||
///
|
||||
/// The first route '/' is mapped to [HomeScreen], and the second route
|
||||
/// '/details' is mapped to [DetailsScreen].
|
||||
///
|
||||
/// The buttons use context.go() to navigate to each destination. On mobile
|
||||
/// devices, each destination is deep-linkable and on the web, can be navigated
|
||||
/// to using the address bar.
|
||||
void main() => runApp(const MyApp());
|
||||
|
||||
/// The route configuration.
|
||||
final GoRouter _router = GoRouter(
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (BuildContext context, GoRouterState state) {
|
||||
return const HomeScreen();
|
||||
},
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
path: 'details',
|
||||
builder: (BuildContext context, GoRouterState state) {
|
||||
return const DetailsScreen();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
/// The main app.
|
||||
class MyApp extends StatelessWidget {
|
||||
/// Constructs a [MyApp]
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp.router(
|
||||
routerConfig: _router,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The home screen
|
||||
class HomeScreen extends StatelessWidget {
|
||||
/// Constructs a [HomeScreen]
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Home Screen')),
|
||||
body: Center(
|
||||
child: ElevatedButton(
|
||||
onPressed: () => context.go('/details'),
|
||||
child: const Text('Go to the Details screen'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The details screen
|
||||
class DetailsScreen extends StatelessWidget {
|
||||
/// Constructs a [DetailsScreen]
|
||||
const DetailsScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Details Screen')),
|
||||
body: Center(
|
||||
child: ElevatedButton(
|
||||
onPressed: () => context.go('/'),
|
||||
child: const Text('Go back to the Home screen'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,181 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
// This scenario demonstrates how to navigate using named locations instead of
|
||||
// URLs.
|
||||
//
|
||||
// Instead of hardcoding the URI locations , you can also use the named
|
||||
// locations. To use this API, give a unique name to each GoRoute. The name can
|
||||
// then be used in context.namedLocation to be translate back to the actual URL
|
||||
// location.
|
||||
|
||||
/// Family data class.
|
||||
class Family {
|
||||
/// Create a family.
|
||||
const Family({required this.name, required this.people});
|
||||
|
||||
/// The last name of the family.
|
||||
final String name;
|
||||
|
||||
/// The people in the family.
|
||||
final Map<String, Person> people;
|
||||
}
|
||||
|
||||
/// Person data class.
|
||||
class Person {
|
||||
/// Creates a person.
|
||||
const Person({required this.name, required this.age});
|
||||
|
||||
/// The first name of the person.
|
||||
final String name;
|
||||
|
||||
/// The age of the person.
|
||||
final int age;
|
||||
}
|
||||
|
||||
const Map<String, Family> _families = <String, Family>{
|
||||
'f1': Family(
|
||||
name: 'Doe',
|
||||
people: <String, Person>{
|
||||
'p1': Person(name: 'Jane', age: 23),
|
||||
'p2': Person(name: 'John', age: 6),
|
||||
},
|
||||
),
|
||||
'f2': Family(
|
||||
name: 'Wong',
|
||||
people: <String, Person>{
|
||||
'p1': Person(name: 'June', age: 51),
|
||||
'p2': Person(name: 'Xin', age: 44),
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
void main() => runApp(App());
|
||||
|
||||
/// The main app.
|
||||
class App extends StatelessWidget {
|
||||
/// Creates an [App].
|
||||
App({super.key});
|
||||
|
||||
/// The title of the app.
|
||||
static const String title = 'GoRouter Example: Named Routes';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => MaterialApp.router(
|
||||
routerConfig: _router,
|
||||
title: title,
|
||||
debugShowCheckedModeBanner: false,
|
||||
);
|
||||
|
||||
late final GoRouter _router = GoRouter(
|
||||
debugLogDiagnostics: true,
|
||||
routes: <GoRoute>[
|
||||
GoRoute(
|
||||
name: 'home',
|
||||
path: '/',
|
||||
builder: (BuildContext context, GoRouterState state) =>
|
||||
const HomeScreen(),
|
||||
routes: <GoRoute>[
|
||||
GoRoute(
|
||||
name: 'family',
|
||||
path: 'family/:fid',
|
||||
builder: (BuildContext context, GoRouterState state) =>
|
||||
FamilyScreen(fid: state.pathParameters['fid']!),
|
||||
routes: <GoRoute>[
|
||||
GoRoute(
|
||||
name: 'person',
|
||||
path: 'person/:pid',
|
||||
builder: (BuildContext context, GoRouterState state) {
|
||||
return PersonScreen(
|
||||
fid: state.pathParameters['fid']!,
|
||||
pid: state.pathParameters['pid']!);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// The home screen that shows a list of families.
|
||||
class HomeScreen extends StatelessWidget {
|
||||
/// Creates a [HomeScreen].
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(App.title),
|
||||
),
|
||||
body: ListView(
|
||||
children: <Widget>[
|
||||
for (final MapEntry<String, Family> entry in _families.entries)
|
||||
ListTile(
|
||||
title: Text(entry.value.name),
|
||||
onTap: () => context.go(context.namedLocation('family',
|
||||
pathParameters: <String, String>{'fid': entry.key})),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The screen that shows a list of persons in a family.
|
||||
class FamilyScreen extends StatelessWidget {
|
||||
/// Creates a [FamilyScreen].
|
||||
const FamilyScreen({required this.fid, super.key});
|
||||
|
||||
/// The id family to display.
|
||||
final String fid;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Map<String, Person> people = _families[fid]!.people;
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(_families[fid]!.name)),
|
||||
body: ListView(
|
||||
children: <Widget>[
|
||||
for (final MapEntry<String, Person> entry in people.entries)
|
||||
ListTile(
|
||||
title: Text(entry.value.name),
|
||||
onTap: () => context.go(context.namedLocation(
|
||||
'person',
|
||||
pathParameters: <String, String>{'fid': fid, 'pid': entry.key},
|
||||
queryParameters: <String, String>{'qid': 'quid'},
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The person screen.
|
||||
class PersonScreen extends StatelessWidget {
|
||||
/// Creates a [PersonScreen].
|
||||
const PersonScreen({required this.fid, required this.pid, super.key});
|
||||
|
||||
/// The id of family this person belong to.
|
||||
final String fid;
|
||||
|
||||
/// The id of the person to be displayed.
|
||||
final String pid;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Family family = _families[fid]!;
|
||||
final Person person = family.people[pid]!;
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(person.name)),
|
||||
body: Text('${person.name} ${family.name} is ${person.age} years old'),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,460 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
final GlobalKey<NavigatorState> _rootNavigatorKey =
|
||||
GlobalKey<NavigatorState>(debugLabel: 'root');
|
||||
final GlobalKey<NavigatorState> _tabANavigatorKey =
|
||||
GlobalKey<NavigatorState>(debugLabel: 'tabANav');
|
||||
|
||||
// This example demonstrates how to setup nested navigation using a
|
||||
// BottomNavigationBar, where each bar item uses its own persistent navigator,
|
||||
// i.e. navigation state is maintained separately for each item. This setup also
|
||||
// enables deep linking into nested pages.
|
||||
//
|
||||
// This example also demonstrates how build a nested shell with a custom
|
||||
// container for the branch Navigators (in this case a TabBarView).
|
||||
|
||||
void main() {
|
||||
runApp(NestedTabNavigationExampleApp());
|
||||
}
|
||||
|
||||
/// An example demonstrating how to use nested navigators
|
||||
class NestedTabNavigationExampleApp extends StatelessWidget {
|
||||
/// Creates a NestedTabNavigationExampleApp
|
||||
NestedTabNavigationExampleApp({super.key});
|
||||
|
||||
final GoRouter _router = GoRouter(
|
||||
navigatorKey: _rootNavigatorKey,
|
||||
initialLocation: '/a',
|
||||
routes: <RouteBase>[
|
||||
StatefulShellRoute(
|
||||
builder: (BuildContext context, GoRouterState state,
|
||||
StatefulNavigationShell navigationShell) {
|
||||
// This nested StatefulShellRoute demonstrates the use of a
|
||||
// custom container for the branch Navigators. In this implementation,
|
||||
// no customization is done in the builder function (navigationShell
|
||||
// itself is simply used as the Widget for the route). Instead, the
|
||||
// navigatorContainerBuilder function below is provided to
|
||||
// customize the container for the branch Navigators.
|
||||
return navigationShell;
|
||||
},
|
||||
navigatorContainerBuilder: (BuildContext context,
|
||||
StatefulNavigationShell navigationShell, List<Widget> children) {
|
||||
// Returning a customized container for the branch
|
||||
// Navigators (i.e. the `List<Widget> children` argument).
|
||||
//
|
||||
// See ScaffoldWithNavBar for more details on how the children
|
||||
// are managed (using AnimatedBranchContainer).
|
||||
return ScaffoldWithNavBar(
|
||||
navigationShell: navigationShell, children: children);
|
||||
},
|
||||
branches: <StatefulShellBranch>[
|
||||
// The route branch for the first tab of the bottom navigation bar.
|
||||
StatefulShellBranch(
|
||||
navigatorKey: _tabANavigatorKey,
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
// The screen to display as the root in the first tab of the
|
||||
// bottom navigation bar.
|
||||
path: '/a',
|
||||
builder: (BuildContext context, GoRouterState state) =>
|
||||
const RootScreenA(),
|
||||
routes: <RouteBase>[
|
||||
// The details screen to display stacked on navigator of the
|
||||
// first tab. This will cover screen A but not the application
|
||||
// shell (bottom navigation bar).
|
||||
GoRoute(
|
||||
path: 'details',
|
||||
builder: (BuildContext context, GoRouterState state) =>
|
||||
const DetailsScreen(label: 'A'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// The route branch for the third tab of the bottom navigation bar.
|
||||
StatefulShellBranch(
|
||||
// StatefulShellBranch will automatically use the first descendant
|
||||
// GoRoute as the initial location of the branch. If another route
|
||||
// is desired, specify the location of it using the defaultLocation
|
||||
// parameter.
|
||||
// defaultLocation: '/c2',
|
||||
routes: <RouteBase>[
|
||||
StatefulShellRoute(
|
||||
builder: (BuildContext context, GoRouterState state,
|
||||
StatefulNavigationShell navigationShell) {
|
||||
// Just like with the top level StatefulShellRoute, no
|
||||
// customization is done in the builder function.
|
||||
return navigationShell;
|
||||
},
|
||||
navigatorContainerBuilder: (BuildContext context,
|
||||
StatefulNavigationShell navigationShell,
|
||||
List<Widget> children) {
|
||||
// Returning a customized container for the branch
|
||||
// Navigators (i.e. the `List<Widget> children` argument).
|
||||
//
|
||||
// See TabbedRootScreen for more details on how the children
|
||||
// are managed (in a TabBarView).
|
||||
return TabbedRootScreen(
|
||||
navigationShell: navigationShell, children: children);
|
||||
},
|
||||
// This bottom tab uses a nested shell, wrapping sub routes in a
|
||||
// top TabBar.
|
||||
branches: <StatefulShellBranch>[
|
||||
StatefulShellBranch(routes: <GoRoute>[
|
||||
GoRoute(
|
||||
path: '/b1',
|
||||
builder: (BuildContext context, GoRouterState state) =>
|
||||
const TabScreen(
|
||||
label: 'B1', detailsPath: '/b1/details'),
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
path: 'details',
|
||||
builder:
|
||||
(BuildContext context, GoRouterState state) =>
|
||||
const DetailsScreen(
|
||||
label: 'B1',
|
||||
withScaffold: false,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]),
|
||||
StatefulShellBranch(routes: <GoRoute>[
|
||||
GoRoute(
|
||||
path: '/b2',
|
||||
builder: (BuildContext context, GoRouterState state) =>
|
||||
const TabScreen(
|
||||
label: 'B2', detailsPath: '/b2/details'),
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
path: 'details',
|
||||
builder:
|
||||
(BuildContext context, GoRouterState state) =>
|
||||
const DetailsScreen(
|
||||
label: 'B2',
|
||||
withScaffold: false,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp.router(
|
||||
title: 'Flutter Demo',
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.blue,
|
||||
),
|
||||
routerConfig: _router,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the "shell" for the app by building a Scaffold with a
|
||||
/// BottomNavigationBar, where [child] is placed in the body of the Scaffold.
|
||||
class ScaffoldWithNavBar extends StatelessWidget {
|
||||
/// Constructs an [ScaffoldWithNavBar].
|
||||
const ScaffoldWithNavBar({
|
||||
required this.navigationShell,
|
||||
required this.children,
|
||||
Key? key,
|
||||
}) : super(key: key ?? const ValueKey<String>('ScaffoldWithNavBar'));
|
||||
|
||||
/// The navigation shell and container for the branch Navigators.
|
||||
final StatefulNavigationShell navigationShell;
|
||||
|
||||
/// The children (branch Navigators) to display in a custom container
|
||||
/// ([AnimatedBranchContainer]).
|
||||
final List<Widget> children;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: AnimatedBranchContainer(
|
||||
currentIndex: navigationShell.currentIndex,
|
||||
children: children,
|
||||
),
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
// Here, the items of BottomNavigationBar are hard coded. In a real
|
||||
// world scenario, the items would most likely be generated from the
|
||||
// branches of the shell route, which can be fetched using
|
||||
// `navigationShell.route.branches`.
|
||||
items: const <BottomNavigationBarItem>[
|
||||
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'),
|
||||
],
|
||||
currentIndex: navigationShell.currentIndex,
|
||||
onTap: (int index) => _onTap(context, index),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Navigate to the current location of the branch at the provided index when
|
||||
/// tapping an item in the BottomNavigationBar.
|
||||
void _onTap(BuildContext context, int index) {
|
||||
// When navigating to a new branch, it's recommended to use the goBranch
|
||||
// method, as doing so makes sure the last navigation state of the
|
||||
// Navigator for the branch is restored.
|
||||
navigationShell.goBranch(
|
||||
index,
|
||||
// A common pattern when using bottom navigation bars is to support
|
||||
// navigating to the initial location when tapping the item that is
|
||||
// already active. This example demonstrates how to support this behavior,
|
||||
// using the initialLocation parameter of goBranch.
|
||||
initialLocation: index == navigationShell.currentIndex,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom branch Navigator container that provides animated transitions
|
||||
/// when switching branches.
|
||||
class AnimatedBranchContainer extends StatelessWidget {
|
||||
/// Creates a AnimatedBranchContainer
|
||||
const AnimatedBranchContainer(
|
||||
{super.key, required this.currentIndex, required this.children});
|
||||
|
||||
/// The index (in [children]) of the branch Navigator to display.
|
||||
final int currentIndex;
|
||||
|
||||
/// The children (branch Navigators) to display in this container.
|
||||
final List<Widget> children;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: children.mapIndexed(
|
||||
(int index, Widget navigator) {
|
||||
return AnimatedScale(
|
||||
scale: index == currentIndex ? 1 : 1.5,
|
||||
duration: const Duration(milliseconds: 400),
|
||||
child: AnimatedOpacity(
|
||||
opacity: index == currentIndex ? 1 : 0,
|
||||
duration: const Duration(milliseconds: 400),
|
||||
child: _branchNavigatorWrapper(index, navigator),
|
||||
),
|
||||
);
|
||||
},
|
||||
).toList());
|
||||
}
|
||||
|
||||
Widget _branchNavigatorWrapper(int index, Widget navigator) => IgnorePointer(
|
||||
ignoring: index != currentIndex,
|
||||
child: TickerMode(
|
||||
enabled: index == currentIndex,
|
||||
child: navigator,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Widget for the root page for the first section of the bottom navigation bar.
|
||||
class RootScreenA extends StatelessWidget {
|
||||
/// Creates a RootScreenA
|
||||
const RootScreenA({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Root of section A'),
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text('Screen A', style: Theme.of(context).textTheme.titleLarge),
|
||||
const Padding(padding: EdgeInsets.all(4)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
GoRouter.of(context).go('/a/details');
|
||||
},
|
||||
child: const Text('View details'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The details screen for either the A or B screen.
|
||||
class DetailsScreen extends StatefulWidget {
|
||||
/// Constructs a [DetailsScreen].
|
||||
const DetailsScreen({
|
||||
required this.label,
|
||||
this.param,
|
||||
this.withScaffold = true,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// The label to display in the center of the screen.
|
||||
final String label;
|
||||
|
||||
/// Optional param
|
||||
final String? param;
|
||||
|
||||
/// Wrap in scaffold
|
||||
final bool withScaffold;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => DetailsScreenState();
|
||||
}
|
||||
|
||||
/// The state for DetailsScreen
|
||||
class DetailsScreenState extends State<DetailsScreen> {
|
||||
int _counter = 0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.withScaffold) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Details Screen - ${widget.label}'),
|
||||
),
|
||||
body: _build(context),
|
||||
);
|
||||
} else {
|
||||
return Container(
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: _build(context),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _build(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text('Details for ${widget.label} - Counter: $_counter',
|
||||
style: Theme.of(context).textTheme.titleLarge),
|
||||
const Padding(padding: EdgeInsets.all(4)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_counter++;
|
||||
});
|
||||
},
|
||||
child: const Text('Increment counter'),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(8)),
|
||||
if (widget.param != null)
|
||||
Text('Parameter: ${widget.param!}',
|
||||
style: Theme.of(context).textTheme.titleMedium),
|
||||
const Padding(padding: EdgeInsets.all(8)),
|
||||
if (!widget.withScaffold) ...<Widget>[
|
||||
const Padding(padding: EdgeInsets.all(16)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
GoRouter.of(context).pop();
|
||||
},
|
||||
child: const Text('< Back',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a nested shell using a [TabBar] and [TabBarView].
|
||||
class TabbedRootScreen extends StatefulWidget {
|
||||
/// Constructs a TabbedRootScreen
|
||||
const TabbedRootScreen(
|
||||
{required this.navigationShell, required this.children, super.key});
|
||||
|
||||
/// The current state of the parent StatefulShellRoute.
|
||||
final StatefulNavigationShell navigationShell;
|
||||
|
||||
/// The children (branch Navigators) to display in the [TabBarView].
|
||||
final List<Widget> children;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TabbedRootScreenState();
|
||||
}
|
||||
|
||||
class _TabbedRootScreenState extends State<TabbedRootScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final TabController _tabController = TabController(
|
||||
length: widget.children.length,
|
||||
vsync: this,
|
||||
initialIndex: widget.navigationShell.currentIndex);
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant TabbedRootScreen oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_tabController.index = widget.navigationShell.currentIndex;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final List<Tab> tabs = widget.children
|
||||
.mapIndexed((int i, _) => Tab(text: 'Tab ${i + 1}'))
|
||||
.toList();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Root of Section B (nested TabBar shell)'),
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
tabs: tabs,
|
||||
onTap: (int tappedIndex) => _onTabTap(context, tappedIndex),
|
||||
)),
|
||||
body: TabBarView(
|
||||
controller: _tabController,
|
||||
children: widget.children,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onTabTap(BuildContext context, int index) {
|
||||
widget.navigationShell.goBranch(index);
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget for the pages in the top tab bar.
|
||||
class TabScreen extends StatelessWidget {
|
||||
/// Creates a RootScreen
|
||||
const TabScreen({required this.label, required this.detailsPath, super.key});
|
||||
|
||||
/// The label
|
||||
final String label;
|
||||
|
||||
/// The path to the detail page
|
||||
final String detailsPath;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text('Screen $label', style: Theme.of(context).textTheme.titleLarge),
|
||||
const Padding(padding: EdgeInsets.all(4)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
GoRouter.of(context).go(detailsPath);
|
||||
},
|
||||
child: const Text('View details'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
void main() => runApp(App());
|
||||
|
||||
/// The main app.
|
||||
class App extends StatelessWidget {
|
||||
/// Creates an [App].
|
||||
App({super.key});
|
||||
|
||||
/// The title of the app.
|
||||
static const String title = 'GoRouter Example: Custom Error Screen';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => MaterialApp.router(
|
||||
routerConfig: _router,
|
||||
title: title,
|
||||
);
|
||||
|
||||
final GoRouter _router = GoRouter(
|
||||
routes: <GoRoute>[
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (BuildContext context, GoRouterState state) =>
|
||||
const Page1Screen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/page2',
|
||||
builder: (BuildContext context, GoRouterState state) =>
|
||||
const Page2Screen(),
|
||||
),
|
||||
],
|
||||
errorBuilder: (BuildContext context, GoRouterState state) =>
|
||||
ErrorScreen(state.error!),
|
||||
);
|
||||
}
|
||||
|
||||
/// The screen of the first page.
|
||||
class Page1Screen extends StatelessWidget {
|
||||
/// Creates a [Page1Screen].
|
||||
const Page1Screen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(title: const Text(App.title)),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
ElevatedButton(
|
||||
onPressed: () => context.go('/page2'),
|
||||
child: const Text('Go to page 2'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// The screen of the second page.
|
||||
class Page2Screen extends StatelessWidget {
|
||||
/// Creates a [Page2Screen].
|
||||
const Page2Screen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(title: const Text(App.title)),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
ElevatedButton(
|
||||
onPressed: () => context.go('/'),
|
||||
child: const Text('Go to home page'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// The screen of the error page.
|
||||
class ErrorScreen extends StatelessWidget {
|
||||
/// Creates an [ErrorScreen].
|
||||
const ErrorScreen(this.error, {super.key});
|
||||
|
||||
/// The error to display.
|
||||
final Exception error;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(title: const Text('My "Page Not Found" Screen')),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
SelectableText(error.toString()),
|
||||
TextButton(
|
||||
onPressed: () => context.go('/'),
|
||||
child: const Text('Home'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
/// Family data class.
|
||||
class Family {
|
||||
/// Create a family.
|
||||
const Family({required this.name, required this.people});
|
||||
|
||||
/// The last name of the family.
|
||||
final String name;
|
||||
|
||||
/// The people in the family.
|
||||
final Map<String, Person> people;
|
||||
}
|
||||
|
||||
/// Person data class.
|
||||
class Person {
|
||||
/// Creates a person.
|
||||
const Person({required this.name, required this.age});
|
||||
|
||||
/// The first name of the person.
|
||||
final String name;
|
||||
|
||||
/// The age of the person.
|
||||
final int age;
|
||||
}
|
||||
|
||||
const Map<String, Family> _families = <String, Family>{
|
||||
'f1': Family(
|
||||
name: 'Doe',
|
||||
people: <String, Person>{
|
||||
'p1': Person(name: 'Jane', age: 23),
|
||||
'p2': Person(name: 'John', age: 6),
|
||||
},
|
||||
),
|
||||
'f2': Family(
|
||||
name: 'Wong',
|
||||
people: <String, Person>{
|
||||
'p1': Person(name: 'June', age: 51),
|
||||
'p2': Person(name: 'Xin', age: 44),
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
void main() => runApp(App());
|
||||
|
||||
/// The main app.
|
||||
class App extends StatelessWidget {
|
||||
/// Creates an [App].
|
||||
App({super.key});
|
||||
|
||||
/// The title of the app.
|
||||
static const String title = 'GoRouter Example: Extra Parameter';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => MaterialApp.router(
|
||||
routerConfig: _router,
|
||||
title: title,
|
||||
);
|
||||
|
||||
late final GoRouter _router = GoRouter(
|
||||
routes: <GoRoute>[
|
||||
GoRoute(
|
||||
name: 'home',
|
||||
path: '/',
|
||||
builder: (BuildContext context, GoRouterState state) =>
|
||||
const HomeScreen(),
|
||||
routes: <GoRoute>[
|
||||
GoRoute(
|
||||
name: 'family',
|
||||
path: 'family',
|
||||
builder: (BuildContext context, GoRouterState state) {
|
||||
final Map<String, Object> params =
|
||||
state.extra! as Map<String, String>;
|
||||
final String fid = params['fid']! as String;
|
||||
return FamilyScreen(fid: fid);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// The home screen that shows a list of families.
|
||||
class HomeScreen extends StatelessWidget {
|
||||
/// Creates a [HomeScreen].
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(title: const Text(App.title)),
|
||||
body: ListView(
|
||||
children: <Widget>[
|
||||
for (final MapEntry<String, Family> entry in _families.entries)
|
||||
ListTile(
|
||||
title: Text(entry.value.name),
|
||||
onTap: () => context.goNamed('family',
|
||||
extra: <String, String>{'fid': entry.key}),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// The screen that shows a list of persons in a family.
|
||||
class FamilyScreen extends StatelessWidget {
|
||||
/// Creates a [FamilyScreen].
|
||||
const FamilyScreen({required this.fid, super.key});
|
||||
|
||||
/// The family to display.
|
||||
final String fid;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Map<String, Person> people = _families[fid]!.people;
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(_families[fid]!.name)),
|
||||
body: ListView(
|
||||
children: <Widget>[
|
||||
for (final Person p in people.values)
|
||||
ListTile(
|
||||
title: Text(p.name),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
void main() => runApp(App());
|
||||
|
||||
/// The main app.
|
||||
class App extends StatelessWidget {
|
||||
/// Creates an [App].
|
||||
App({super.key});
|
||||
|
||||
/// The title of the app.
|
||||
static const String title = 'GoRouter Example: Initial Location';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => MaterialApp.router(
|
||||
routerConfig: _router,
|
||||
title: title,
|
||||
);
|
||||
|
||||
final GoRouter _router = GoRouter(
|
||||
initialLocation: '/page3',
|
||||
routes: <GoRoute>[
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (BuildContext context, GoRouterState state) =>
|
||||
const Page1Screen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/page2',
|
||||
builder: (BuildContext context, GoRouterState state) =>
|
||||
const Page2Screen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/page3',
|
||||
builder: (BuildContext context, GoRouterState state) =>
|
||||
const Page3Screen(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// The screen of the first page.
|
||||
class Page1Screen extends StatelessWidget {
|
||||
/// Creates a [Page1Screen].
|
||||
const Page1Screen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(title: const Text(App.title)),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
ElevatedButton(
|
||||
onPressed: () => context.go('/page2'),
|
||||
child: const Text('Go to page 2'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// The screen of the second page.
|
||||
class Page2Screen extends StatelessWidget {
|
||||
/// Creates a [Page2Screen].
|
||||
const Page2Screen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(title: const Text(App.title)),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
ElevatedButton(
|
||||
onPressed: () => context.go('/'),
|
||||
child: const Text('Go to home page'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// The screen of the third page.
|
||||
class Page3Screen extends StatelessWidget {
|
||||
/// Creates a [Page3Screen].
|
||||
const Page3Screen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(title: const Text(App.title)),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
ElevatedButton(
|
||||
onPressed: () => context.go('/page2'),
|
||||
child: const Text('Go to page 2'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
void main() => runApp(App());
|
||||
|
||||
/// The main app.
|
||||
class App extends StatelessWidget {
|
||||
/// Creates an [App].
|
||||
App({super.key});
|
||||
|
||||
/// The title of the app.
|
||||
static const String title = 'GoRouter Example: Navigator Observer';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => MaterialApp.router(
|
||||
routerConfig: _router,
|
||||
title: title,
|
||||
);
|
||||
|
||||
final GoRouter _router = GoRouter(
|
||||
observers: <NavigatorObserver>[MyNavObserver()],
|
||||
routes: <GoRoute>[
|
||||
GoRoute(
|
||||
// if there's no name, path will be used as name for observers
|
||||
path: '/',
|
||||
builder: (BuildContext context, GoRouterState state) =>
|
||||
const Page1Screen(),
|
||||
routes: <GoRoute>[
|
||||
GoRoute(
|
||||
name: 'page2',
|
||||
path: 'page2/:p1',
|
||||
builder: (BuildContext context, GoRouterState state) =>
|
||||
const Page2Screen(),
|
||||
routes: <GoRoute>[
|
||||
GoRoute(
|
||||
name: 'page3',
|
||||
path: 'page3',
|
||||
builder: (BuildContext context, GoRouterState state) =>
|
||||
const Page3Screen(),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// The Navigator observer.
|
||||
class MyNavObserver extends NavigatorObserver {
|
||||
/// Creates a [MyNavObserver].
|
||||
MyNavObserver() {
|
||||
log.onRecord.listen((LogRecord e) => debugPrint('$e'));
|
||||
}
|
||||
|
||||
/// The logged message.
|
||||
final Logger log = Logger('MyNavObserver');
|
||||
|
||||
@override
|
||||
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) =>
|
||||
log.info('didPush: ${route.str}, previousRoute= ${previousRoute?.str}');
|
||||
|
||||
@override
|
||||
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) =>
|
||||
log.info('didPop: ${route.str}, previousRoute= ${previousRoute?.str}');
|
||||
|
||||
@override
|
||||
void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) =>
|
||||
log.info('didRemove: ${route.str}, previousRoute= ${previousRoute?.str}');
|
||||
|
||||
@override
|
||||
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) =>
|
||||
log.info('didReplace: new= ${newRoute?.str}, old= ${oldRoute?.str}');
|
||||
|
||||
@override
|
||||
void didStartUserGesture(
|
||||
Route<dynamic> route,
|
||||
Route<dynamic>? previousRoute,
|
||||
) =>
|
||||
log.info('didStartUserGesture: ${route.str}, '
|
||||
'previousRoute= ${previousRoute?.str}');
|
||||
|
||||
@override
|
||||
void didStopUserGesture() => log.info('didStopUserGesture');
|
||||
}
|
||||
|
||||
extension on Route<dynamic> {
|
||||
String get str => 'route(${settings.name}: ${settings.arguments})';
|
||||
}
|
||||
|
||||
/// The screen of the first page.
|
||||
class Page1Screen extends StatelessWidget {
|
||||
/// Creates a [Page1Screen].
|
||||
const Page1Screen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(title: const Text(App.title)),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
ElevatedButton(
|
||||
onPressed: () => context.goNamed(
|
||||
'page2',
|
||||
pathParameters: <String, String>{'p1': 'pv1'},
|
||||
queryParameters: <String, String>{'q1': 'qv1'},
|
||||
),
|
||||
child: const Text('Go to page 2'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// The screen of the second page.
|
||||
class Page2Screen extends StatelessWidget {
|
||||
/// Creates a [Page2Screen].
|
||||
const Page2Screen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(title: const Text(App.title)),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
ElevatedButton(
|
||||
onPressed: () => context.goNamed(
|
||||
'page3',
|
||||
pathParameters: <String, String>{'p1': 'pv2'},
|
||||
),
|
||||
child: const Text('Go to page 3'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// The screen of the third page.
|
||||
class Page3Screen extends StatelessWidget {
|
||||
/// Creates a [Page3Screen].
|
||||
const Page3Screen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(title: const Text(App.title)),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
ElevatedButton(
|
||||
onPressed: () => context.go('/'),
|
||||
child: const Text('Go to home page'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
void main() => runApp(App());
|
||||
|
||||
/// The main app.
|
||||
class App extends StatelessWidget {
|
||||
/// Creates an [App].
|
||||
App({super.key});
|
||||
|
||||
/// The title of the app.
|
||||
static const String title = 'GoRouter Example: Push';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => MaterialApp.router(
|
||||
routerConfig: _router,
|
||||
title: title,
|
||||
);
|
||||
|
||||
late final GoRouter _router = GoRouter(
|
||||
routes: <GoRoute>[
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (BuildContext context, GoRouterState state) =>
|
||||
const Page1ScreenWithPush(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/page2',
|
||||
builder: (BuildContext context, GoRouterState state) =>
|
||||
Page2ScreenWithPush(
|
||||
int.parse(state.uri.queryParameters['push-count']!),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// The screen of the first page.
|
||||
class Page1ScreenWithPush extends StatelessWidget {
|
||||
/// Creates a [Page1ScreenWithPush].
|
||||
const Page1ScreenWithPush({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(title: const Text('${App.title}: page 1')),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
ElevatedButton(
|
||||
onPressed: () => context.push('/page2?push-count=1'),
|
||||
child: const Text('Push page 2'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// The screen of the second page.
|
||||
class Page2ScreenWithPush extends StatelessWidget {
|
||||
/// Creates a [Page2ScreenWithPush].
|
||||
const Page2ScreenWithPush(this.pushCount, {super.key});
|
||||
|
||||
/// The push count.
|
||||
final int pushCount;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('${App.title}: page 2 w/ push count $pushCount'),
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: ElevatedButton(
|
||||
onPressed: () => context.go('/'),
|
||||
child: const Text('Go to home page'),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: ElevatedButton(
|
||||
onPressed: () => context.push(
|
||||
'/page2?push-count=${pushCount + 1}',
|
||||
),
|
||||
child: const Text('Push page 2 (again)'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
void main() => runApp(App());
|
||||
|
||||
/// The main app.
|
||||
class App extends StatelessWidget {
|
||||
/// Creates an [App].
|
||||
App({super.key});
|
||||
|
||||
/// The title of the app.
|
||||
static const String title = 'GoRouter Example: Router neglect';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => MaterialApp.router(
|
||||
routerConfig: _router,
|
||||
title: title,
|
||||
);
|
||||
|
||||
final GoRouter _router = GoRouter(
|
||||
// turn off history tracking in the browser for this navigation
|
||||
routerNeglect: true,
|
||||
routes: <GoRoute>[
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (BuildContext context, GoRouterState state) =>
|
||||
const Page1Screen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/page2',
|
||||
builder: (BuildContext context, GoRouterState state) =>
|
||||
const Page2Screen(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// The screen of the first page.
|
||||
class Page1Screen extends StatelessWidget {
|
||||
/// Creates a [Page1Screen].
|
||||
const Page1Screen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(title: const Text(App.title)),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
ElevatedButton(
|
||||
onPressed: () => context.go('/page2'),
|
||||
child: const Text('Go to page 2'),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
ElevatedButton(
|
||||
// turn off history tracking in the browser for this navigation;
|
||||
// note that this isn't necessary when you've set routerNeglect
|
||||
// but it does illustrate the technique
|
||||
onPressed: () => Router.neglect(
|
||||
context,
|
||||
() => context.push('/page2'),
|
||||
),
|
||||
child: const Text('Push page 2'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// The screen of the second page.
|
||||
class Page2Screen extends StatelessWidget {
|
||||
/// Creates a [Page2Screen].
|
||||
const Page2Screen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(title: const Text(App.title)),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
ElevatedButton(
|
||||
onPressed: () => context.go('/'),
|
||||
child: const Text('Go to home page'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
void main() => runApp(
|
||||
const RootRestorationScope(restorationId: 'root', child: App()),
|
||||
);
|
||||
|
||||
/// The main app.
|
||||
class App extends StatefulWidget {
|
||||
/// Creates an [App].
|
||||
const App({super.key});
|
||||
|
||||
/// The title of the app.
|
||||
static const String title = 'GoRouter Example: State Restoration';
|
||||
|
||||
@override
|
||||
State<App> createState() => _AppState();
|
||||
}
|
||||
|
||||
class _AppState extends State<App> with RestorationMixin {
|
||||
@override
|
||||
String get restorationId => 'wrapper';
|
||||
|
||||
@override
|
||||
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
|
||||
// Implement restoreState for your app
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => MaterialApp.router(
|
||||
routerConfig: _router,
|
||||
title: App.title,
|
||||
restorationScopeId: 'app',
|
||||
);
|
||||
|
||||
final GoRouter _router = GoRouter(
|
||||
routes: <GoRoute>[
|
||||
// restorationId set for the route automatically
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (BuildContext context, GoRouterState state) =>
|
||||
const Page1Screen(),
|
||||
),
|
||||
|
||||
// restorationId set for the route automatically
|
||||
GoRoute(
|
||||
path: '/page2',
|
||||
builder: (BuildContext context, GoRouterState state) =>
|
||||
const Page2Screen(),
|
||||
),
|
||||
],
|
||||
restorationScopeId: 'router',
|
||||
);
|
||||
}
|
||||
|
||||
/// The screen of the first page.
|
||||
class Page1Screen extends StatelessWidget {
|
||||
/// Creates a [Page1Screen].
|
||||
const Page1Screen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(title: const Text(App.title)),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
ElevatedButton(
|
||||
onPressed: () => context.go('/page2'),
|
||||
child: const Text('Go to page 2'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// The screen of the second page.
|
||||
class Page2Screen extends StatelessWidget {
|
||||
/// Creates a [Page2Screen].
|
||||
const Page2Screen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(title: const Text(App.title)),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
ElevatedButton(
|
||||
onPressed: () => context.go('/'),
|
||||
child: const Text('Go to home page'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
void main() => runApp(RestorableStatefulShellRouteExampleApp());
|
||||
|
||||
/// An example demonstrating how to use StatefulShellRoute with state
|
||||
/// restoration.
|
||||
class RestorableStatefulShellRouteExampleApp extends StatelessWidget {
|
||||
/// Creates a NestedTabNavigationExampleApp
|
||||
RestorableStatefulShellRouteExampleApp({super.key});
|
||||
|
||||
final GoRouter _router = GoRouter(
|
||||
initialLocation: '/a',
|
||||
restorationScopeId: 'router',
|
||||
routes: <RouteBase>[
|
||||
StatefulShellRoute.indexedStack(
|
||||
restorationScopeId: 'shell1',
|
||||
pageBuilder: (BuildContext context, GoRouterState state,
|
||||
StatefulNavigationShell navigationShell) {
|
||||
return MaterialPage<void>(
|
||||
restorationId: 'shellWidget1',
|
||||
child: ScaffoldWithNavBar(navigationShell: navigationShell));
|
||||
},
|
||||
branches: <StatefulShellBranch>[
|
||||
// The route branch for the first tab of the bottom navigation bar.
|
||||
StatefulShellBranch(
|
||||
restorationScopeId: 'branchA',
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
// The screen to display as the root in the first tab of the
|
||||
// bottom navigation bar.
|
||||
path: '/a',
|
||||
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||
const MaterialPage<void>(
|
||||
restorationId: 'screenA',
|
||||
child:
|
||||
RootScreen(label: 'A', detailsPath: '/a/details')),
|
||||
routes: <RouteBase>[
|
||||
// The details screen to display stacked on navigator of the
|
||||
// first tab. This will cover screen A but not the application
|
||||
// shell (bottom navigation bar).
|
||||
GoRoute(
|
||||
path: 'details',
|
||||
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||
const MaterialPage<void>(
|
||||
restorationId: 'screenADetail',
|
||||
child: DetailsScreen(label: 'A')),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
// The route branch for the second tab of the bottom navigation bar.
|
||||
StatefulShellBranch(
|
||||
restorationScopeId: 'branchB',
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
// The screen to display as the root in the second tab of the
|
||||
// bottom navigation bar.
|
||||
path: '/b',
|
||||
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||
const MaterialPage<void>(
|
||||
restorationId: 'screenB',
|
||||
child:
|
||||
RootScreen(label: 'B', detailsPath: '/b/details')),
|
||||
routes: <RouteBase>[
|
||||
// The details screen to display stacked on navigator of the
|
||||
// first tab. This will cover screen A but not the application
|
||||
// shell (bottom navigation bar).
|
||||
GoRoute(
|
||||
path: 'details',
|
||||
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||
const MaterialPage<void>(
|
||||
restorationId: 'screenBDetail',
|
||||
child: DetailsScreen(label: 'B')),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp.router(
|
||||
restorationScopeId: 'app',
|
||||
title: 'Flutter Demo',
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.blue,
|
||||
),
|
||||
routerConfig: _router,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the "shell" for the app by building a Scaffold with a
|
||||
/// BottomNavigationBar, where [child] is placed in the body of the Scaffold.
|
||||
class ScaffoldWithNavBar extends StatelessWidget {
|
||||
/// Constructs an [ScaffoldWithNavBar].
|
||||
const ScaffoldWithNavBar({
|
||||
required this.navigationShell,
|
||||
Key? key,
|
||||
}) : super(key: key ?? const ValueKey<String>('ScaffoldWithNavBar'));
|
||||
|
||||
/// The navigation shell and container for the branch Navigators.
|
||||
final StatefulNavigationShell navigationShell;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: navigationShell,
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
items: const <BottomNavigationBarItem>[
|
||||
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section A'),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section B'),
|
||||
],
|
||||
currentIndex: navigationShell.currentIndex,
|
||||
onTap: (int tappedIndex) => navigationShell.goBranch(tappedIndex),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget for the root/initial pages in the bottom navigation bar.
|
||||
class RootScreen extends StatelessWidget {
|
||||
/// Creates a RootScreen
|
||||
const RootScreen({
|
||||
required this.label,
|
||||
required this.detailsPath,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// The label
|
||||
final String label;
|
||||
|
||||
/// The path to the detail page
|
||||
final String detailsPath;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Root of section $label'),
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text('Screen $label',
|
||||
style: Theme.of(context).textTheme.titleLarge),
|
||||
const Padding(padding: EdgeInsets.all(4)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
GoRouter.of(context).go(detailsPath);
|
||||
},
|
||||
child: const Text('View details'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The details screen for either the A or B screen.
|
||||
class DetailsScreen extends StatefulWidget {
|
||||
/// Constructs a [DetailsScreen].
|
||||
const DetailsScreen({
|
||||
required this.label,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// The label to display in the center of the screen.
|
||||
final String label;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => DetailsScreenState();
|
||||
}
|
||||
|
||||
/// The state for DetailsScreen
|
||||
class DetailsScreenState extends State<DetailsScreen> with RestorationMixin {
|
||||
final RestorableInt _counter = RestorableInt(0);
|
||||
|
||||
@override
|
||||
String? get restorationId => 'DetailsScreen-${widget.label}';
|
||||
|
||||
@override
|
||||
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
|
||||
registerForRestoration(_counter, 'counter');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_counter.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('Details Screen - ${widget.label}'),
|
||||
),
|
||||
body: _build(context),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _build(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text('Details for ${widget.label} - Counter: ${_counter.value}',
|
||||
style: Theme.of(context).textTheme.titleLarge),
|
||||
const Padding(padding: EdgeInsets.all(4)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_counter.value++;
|
||||
});
|
||||
},
|
||||
child: const Text('Increment counter'),
|
||||
),
|
||||
const Padding(padding: EdgeInsets.all(8)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
void main() => runApp(App());
|
||||
|
||||
/// The main app.
|
||||
class App extends StatelessWidget {
|
||||
/// Creates an [App].
|
||||
App({super.key});
|
||||
|
||||
/// The title of the app.
|
||||
static const String title = 'GoRouter Example: Custom Transitions';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => MaterialApp.router(
|
||||
routerConfig: _router,
|
||||
title: title,
|
||||
);
|
||||
|
||||
final GoRouter _router = GoRouter(
|
||||
routes: <GoRoute>[
|
||||
GoRoute(
|
||||
path: '/',
|
||||
redirect: (_, __) => '/none',
|
||||
),
|
||||
GoRoute(
|
||||
path: '/fade',
|
||||
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||
CustomTransitionPage<void>(
|
||||
key: state.pageKey,
|
||||
child: const ExampleTransitionsScreen(
|
||||
kind: 'fade',
|
||||
color: Colors.red,
|
||||
),
|
||||
transitionsBuilder: (BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
Widget child) =>
|
||||
FadeTransition(opacity: animation, child: child),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/scale',
|
||||
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||
CustomTransitionPage<void>(
|
||||
key: state.pageKey,
|
||||
child: const ExampleTransitionsScreen(
|
||||
kind: 'scale',
|
||||
color: Colors.green,
|
||||
),
|
||||
transitionsBuilder: (BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
Widget child) =>
|
||||
ScaleTransition(scale: animation, child: child),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/slide',
|
||||
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||
CustomTransitionPage<void>(
|
||||
key: state.pageKey,
|
||||
child: const ExampleTransitionsScreen(
|
||||
kind: 'slide',
|
||||
color: Colors.yellow,
|
||||
),
|
||||
transitionsBuilder: (BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
Widget child) =>
|
||||
SlideTransition(
|
||||
position: animation.drive(
|
||||
Tween<Offset>(
|
||||
begin: const Offset(0.25, 0.25),
|
||||
end: Offset.zero,
|
||||
).chain(CurveTween(curve: Curves.easeIn)),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/rotation',
|
||||
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||
CustomTransitionPage<void>(
|
||||
key: state.pageKey,
|
||||
child: const ExampleTransitionsScreen(
|
||||
kind: 'rotation',
|
||||
color: Colors.purple,
|
||||
),
|
||||
transitionsBuilder: (BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
Widget child) =>
|
||||
RotationTransition(turns: animation, child: child),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/none',
|
||||
pageBuilder: (BuildContext context, GoRouterState state) =>
|
||||
NoTransitionPage<void>(
|
||||
key: state.pageKey,
|
||||
child: const ExampleTransitionsScreen(
|
||||
kind: 'none',
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// An Example transitions screen.
|
||||
class ExampleTransitionsScreen extends StatelessWidget {
|
||||
/// Creates an [ExampleTransitionsScreen].
|
||||
const ExampleTransitionsScreen({
|
||||
required this.color,
|
||||
required this.kind,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// The available transition kinds.
|
||||
static final List<String> kinds = <String>[
|
||||
'fade',
|
||||
'scale',
|
||||
'slide',
|
||||
'rotation',
|
||||
'none'
|
||||
];
|
||||
|
||||
/// The color of the container.
|
||||
final Color color;
|
||||
|
||||
/// The transition kind.
|
||||
final String kind;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(title: Text('${App.title}: $kind')),
|
||||
body: Container(
|
||||
color: color,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
for (final String kind in kinds)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: ElevatedButton(
|
||||
onPressed: () => context.go('/$kind'),
|
||||
child: Text('$kind transition'),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
// This scenario demonstrates how to use path parameters and query parameters.
|
||||
//
|
||||
// The route segments that start with ':' are treated as path parameters when
|
||||
// defining GoRoute[s]. The parameter values can be accessed through
|
||||
// GoRouterState.pathParameters.
|
||||
//
|
||||
// The query parameters are automatically stored in GoRouterState.queryParameters.
|
||||
|
||||
/// Family data class.
|
||||
class Family {
|
||||
/// Create a family.
|
||||
const Family({required this.name, required this.people});
|
||||
|
||||
/// The last name of the family.
|
||||
final String name;
|
||||
|
||||
/// The people in the family.
|
||||
final Map<String, Person> people;
|
||||
}
|
||||
|
||||
/// Person data class.
|
||||
class Person {
|
||||
/// Creates a person.
|
||||
const Person({required this.name, required this.age});
|
||||
|
||||
/// The first name of the person.
|
||||
final String name;
|
||||
|
||||
/// The age of the person.
|
||||
final int age;
|
||||
}
|
||||
|
||||
const Map<String, Family> _families = <String, Family>{
|
||||
'f1': Family(
|
||||
name: 'Doe',
|
||||
people: <String, Person>{
|
||||
'p1': Person(name: 'Jane', age: 23),
|
||||
'p2': Person(name: 'John', age: 6),
|
||||
},
|
||||
),
|
||||
'f2': Family(
|
||||
name: 'Wong',
|
||||
people: <String, Person>{
|
||||
'p1': Person(name: 'June', age: 51),
|
||||
'p2': Person(name: 'Xin', age: 44),
|
||||
},
|
||||
),
|
||||
};
|
||||
|
||||
void main() => runApp(App());
|
||||
|
||||
/// The main app.
|
||||
class App extends StatelessWidget {
|
||||
/// Creates an [App].
|
||||
App({super.key});
|
||||
|
||||
/// The title of the app.
|
||||
static const String title = 'GoRouter Example: Query Parameters';
|
||||
|
||||
// add the login info into the tree as app state that can change over time
|
||||
@override
|
||||
Widget build(BuildContext context) => MaterialApp.router(
|
||||
routerConfig: _router,
|
||||
title: title,
|
||||
debugShowCheckedModeBanner: false,
|
||||
);
|
||||
|
||||
late final GoRouter _router = GoRouter(
|
||||
routes: <GoRoute>[
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (BuildContext context, GoRouterState state) =>
|
||||
const HomeScreen(),
|
||||
routes: <GoRoute>[
|
||||
GoRoute(
|
||||
name: 'family',
|
||||
path: 'family/:fid',
|
||||
builder: (BuildContext context, GoRouterState state) {
|
||||
return FamilyScreen(
|
||||
fid: state.pathParameters['fid']!,
|
||||
asc: state.uri.queryParameters['sort'] == 'asc',
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// The home screen that shows a list of families.
|
||||
class HomeScreen extends StatelessWidget {
|
||||
/// Creates a [HomeScreen].
|
||||
const HomeScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(App.title),
|
||||
),
|
||||
body: ListView(
|
||||
children: <Widget>[
|
||||
for (final MapEntry<String, Family> entry in _families.entries)
|
||||
ListTile(
|
||||
title: Text(entry.value.name),
|
||||
onTap: () => context.go('/family/${entry.key}'),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The screen that shows a list of persons in a family.
|
||||
class FamilyScreen extends StatelessWidget {
|
||||
/// Creates a [FamilyScreen].
|
||||
const FamilyScreen({required this.fid, required this.asc, super.key});
|
||||
|
||||
/// The family to display.
|
||||
final String fid;
|
||||
|
||||
/// Whether to sort the name in ascending order.
|
||||
final bool asc;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Map<String, String> newQueries;
|
||||
final List<String> names = _families[fid]!
|
||||
.people
|
||||
.values
|
||||
.map<String>((Person p) => p.name)
|
||||
.toList();
|
||||
names.sort();
|
||||
if (asc) {
|
||||
newQueries = const <String, String>{'sort': 'desc'};
|
||||
} else {
|
||||
newQueries = const <String, String>{'sort': 'asc'};
|
||||
}
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(_families[fid]!.name),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
onPressed: () => context.goNamed('family',
|
||||
pathParameters: <String, String>{'fid': fid},
|
||||
queryParameters: newQueries),
|
||||
tooltip: 'sort ascending or descending',
|
||||
icon: const Icon(Icons.sort),
|
||||
)
|
||||
],
|
||||
),
|
||||
body: ListView(
|
||||
children: <Widget>[
|
||||
for (final String name in asc ? names : names.reversed)
|
||||
ListTile(
|
||||
title: Text(name),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:io' show Platform;
|
||||
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_strategy/url_strategy.dart';
|
||||
// import 'package:window_size/window_size.dart';
|
||||
|
||||
import 'src/app.dart';
|
||||
|
||||
void main() {
|
||||
// Use package:url_strategy until this pull request is released:
|
||||
// https://github.com/flutter/flutter/pull/77103
|
||||
|
||||
// Use to setHashUrlStrategy() to use "/#/" in the address bar (default). Use
|
||||
// setPathUrlStrategy() to use the path. You may need to configure your web
|
||||
// server to redirect all paths to index.html.
|
||||
//
|
||||
// On mobile platforms, both functions are no-ops.
|
||||
setHashUrlStrategy();
|
||||
// setPathUrlStrategy();
|
||||
runApp(const Bookstore());
|
||||
}
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'auth.dart';
|
||||
import 'routing.dart';
|
||||
import 'screens/navigator.dart';
|
||||
|
||||
class Bookstore extends StatefulWidget {
|
||||
const Bookstore({super.key});
|
||||
|
||||
@override
|
||||
State<Bookstore> createState() => _BookstoreState();
|
||||
}
|
||||
|
||||
class _BookstoreState extends State<Bookstore> {
|
||||
late final RouteState _routeState;
|
||||
late final SimpleRouterDelegate _routerDelegate;
|
||||
late final TemplateRouteParser _routeParser;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
/// Configure the parser with all of the app's allowed path templates.
|
||||
_routeParser = TemplateRouteParser(
|
||||
allowedPaths: [
|
||||
'/signin',
|
||||
'/authors',
|
||||
'/settings',
|
||||
'/books/new',
|
||||
'/books/all',
|
||||
'/books/popular',
|
||||
'/book/:bookId',
|
||||
'/author/:authorId',
|
||||
],
|
||||
initialRoute: '/signin',
|
||||
);
|
||||
|
||||
_routeState = RouteState(_routeParser);
|
||||
|
||||
_routerDelegate = SimpleRouterDelegate(
|
||||
routeState: _routeState,
|
||||
);
|
||||
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => RouteStateScope(
|
||||
notifier: _routeState,
|
||||
child: MaterialApp.router(
|
||||
routerDelegate: _routerDelegate,
|
||||
routeInformationParser: _routeParser,
|
||||
// Revert back to pre-Flutter-2.5 transition behavior:
|
||||
// https://github.com/flutter/flutter/issues/82053
|
||||
theme: ThemeData(
|
||||
useMaterial3: true,
|
||||
pageTransitionsTheme: const PageTransitionsTheme(
|
||||
builders: {
|
||||
TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
|
||||
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
|
||||
TargetPlatform.linux: FadeUpwardsPageTransitionsBuilder(),
|
||||
TargetPlatform.macOS: CupertinoPageTransitionsBuilder(),
|
||||
TargetPlatform.windows: FadeUpwardsPageTransitionsBuilder(),
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_routeState.dispose();
|
||||
_routerDelegate.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
// // Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
||||
// // for details. All rights reserved. Use of this source code is governed by a
|
||||
// // BSD-style license that can be found in the LICENSE file.
|
||||
//
|
||||
// import 'package:flutter/widgets.dart';
|
||||
//
|
||||
// /// A mock authentication service
|
||||
// class BookstoreAuth extends ChangeNotifier {
|
||||
// bool _signedIn = false;
|
||||
//
|
||||
// bool get signedIn => _signedIn;
|
||||
//
|
||||
// Future<void> signOut() async {
|
||||
// await Future<void>.delayed(const Duration(milliseconds: 200));
|
||||
// // Sign out.
|
||||
// _signedIn = false;
|
||||
// notifyListeners();
|
||||
// }
|
||||
//
|
||||
// Future<bool> signIn(String username, String password) async {
|
||||
// await Future<void>.delayed(const Duration(milliseconds: 200));
|
||||
//
|
||||
// // Sign in. Allow any password.
|
||||
// _signedIn = true;
|
||||
// notifyListeners();
|
||||
// return _signedIn;
|
||||
// }
|
||||
//
|
||||
// @override
|
||||
// bool operator ==(Object other) =>
|
||||
// other is BookstoreAuth && other._signedIn == _signedIn;
|
||||
//
|
||||
// @override
|
||||
// int get hashCode => _signedIn.hashCode;
|
||||
// }
|
||||
//
|
||||
// class BookstoreAuthScope extends InheritedNotifier<BookstoreAuth> {
|
||||
// const BookstoreAuthScope({
|
||||
// required super.notifier,
|
||||
// required super.child,
|
||||
// super.key,
|
||||
// });
|
||||
//
|
||||
// static BookstoreAuth of(BuildContext context) => context
|
||||
// .dependOnInheritedWidgetOfExactType<BookstoreAuthScope>()!
|
||||
// .notifier!;
|
||||
// }
|
||||
@@ -1,7 +0,0 @@
|
||||
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
export 'data/author.dart';
|
||||
export 'data/book.dart';
|
||||
export 'data/library.dart';
|
||||
@@ -1,13 +0,0 @@
|
||||
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'book.dart';
|
||||
|
||||
class Author {
|
||||
final int id;
|
||||
final String name;
|
||||
final books = <Book>[];
|
||||
|
||||
Author(this.id, this.name);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'author.dart';
|
||||
|
||||
class Book {
|
||||
final int id;
|
||||
final String title;
|
||||
final Author author;
|
||||
final bool isPopular;
|
||||
final bool isNew;
|
||||
|
||||
Book(this.id, this.title, this.isPopular, this.isNew, this.author);
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'author.dart';
|
||||
import 'book.dart';
|
||||
|
||||
final libraryInstance = Library()
|
||||
..addBook(
|
||||
title: 'Left Hand of Darkness',
|
||||
authorName: 'Ursula K. Le Guin',
|
||||
isPopular: true,
|
||||
isNew: true)
|
||||
..addBook(
|
||||
title: 'Too Like the Lightning',
|
||||
authorName: 'Ada Palmer',
|
||||
isPopular: false,
|
||||
isNew: true)
|
||||
..addBook(
|
||||
title: 'Kindred',
|
||||
authorName: 'Octavia E. Butler',
|
||||
isPopular: true,
|
||||
isNew: false)
|
||||
..addBook(
|
||||
title: 'The Lathe of Heaven',
|
||||
authorName: 'Ursula K. Le Guin',
|
||||
isPopular: false,
|
||||
isNew: false);
|
||||
|
||||
class Library {
|
||||
final List<Book> allBooks = [];
|
||||
final List<Author> allAuthors = [];
|
||||
|
||||
void addBook({
|
||||
required String title,
|
||||
required String authorName,
|
||||
required bool isPopular,
|
||||
required bool isNew,
|
||||
}) {
|
||||
var author = allAuthors.firstWhere(
|
||||
(author) => author.name == authorName,
|
||||
orElse: () {
|
||||
final value = Author(allAuthors.length, authorName);
|
||||
allAuthors.add(value);
|
||||
return value;
|
||||
},
|
||||
);
|
||||
var book = Book(allBooks.length, title, isPopular, isNew, author);
|
||||
|
||||
author.books.add(book);
|
||||
allBooks.add(book);
|
||||
}
|
||||
|
||||
List<Book> get popularBooks => [
|
||||
...allBooks.where((book) => book.isPopular),
|
||||
];
|
||||
|
||||
List<Book> get newBooks => [
|
||||
...allBooks.where((book) => book.isNew),
|
||||
];
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
export 'routing/delegate.dart';
|
||||
export 'routing/parsed_route.dart';
|
||||
export 'routing/parser.dart';
|
||||
export 'routing/route_state.dart';
|
||||
@@ -1,47 +0,0 @@
|
||||
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import '../screens/navigator.dart';
|
||||
import 'parsed_route.dart';
|
||||
import 'route_state.dart';
|
||||
|
||||
class SimpleRouterDelegate extends RouterDelegate<ParsedRoute>
|
||||
with ChangeNotifier, PopNavigatorRouterDelegateMixin<ParsedRoute> {
|
||||
final RouteState routeState;
|
||||
|
||||
@override
|
||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
|
||||
SimpleRouterDelegate({
|
||||
required this.routeState,
|
||||
}) {
|
||||
routeState.addListener(notifyListeners);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => BookstoreNavigator(
|
||||
navigatorKey: navigatorKey,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> setNewRoutePath(ParsedRoute configuration) async {
|
||||
routeState.route = configuration;
|
||||
return SynchronousFuture(null);
|
||||
}
|
||||
|
||||
@override
|
||||
ParsedRoute get currentConfiguration => routeState.route;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
routeState.removeListener(notifyListeners);
|
||||
routeState.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:quiver/core.dart';
|
||||
|
||||
import 'parser.dart';
|
||||
|
||||
/// A route path that has been parsed by [TemplateRouteParser].
|
||||
class ParsedRoute {
|
||||
/// The current path location without query parameters. (/book/123)
|
||||
final String path;
|
||||
|
||||
/// The path template (/book/:id)
|
||||
final String pathTemplate;
|
||||
|
||||
/// The path parameters ({id: 123})
|
||||
final Map<String, String> parameters;
|
||||
|
||||
/// The query parameters ({search: abc})
|
||||
final Map<String, String> queryParameters;
|
||||
|
||||
static const _mapEquality = MapEquality<String, String>();
|
||||
|
||||
ParsedRoute(
|
||||
this.path, this.pathTemplate, this.parameters, this.queryParameters);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is ParsedRoute &&
|
||||
other.pathTemplate == pathTemplate &&
|
||||
other.path == path &&
|
||||
_mapEquality.equals(parameters, other.parameters) &&
|
||||
_mapEquality.equals(queryParameters, other.queryParameters);
|
||||
|
||||
@override
|
||||
int get hashCode => hash4(
|
||||
path,
|
||||
pathTemplate,
|
||||
_mapEquality.hash(parameters),
|
||||
_mapEquality.hash(queryParameters),
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() => '<ParsedRoute '
|
||||
'template: $pathTemplate '
|
||||
'path: $path '
|
||||
'parameters: $parameters '
|
||||
'query parameters: $queryParameters>';
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:path_to_regexp/path_to_regexp.dart';
|
||||
|
||||
import 'parsed_route.dart';
|
||||
|
||||
/// Used by [TemplateRouteParser] to guard access to routes.
|
||||
typedef RouteGuard<T> = Future<T> Function(T from);
|
||||
|
||||
/// Parses the URI path into a [ParsedRoute].
|
||||
class TemplateRouteParser extends RouteInformationParser<ParsedRoute> {
|
||||
final List<String> _pathTemplates;
|
||||
final ParsedRoute initialRoute;
|
||||
|
||||
TemplateRouteParser({
|
||||
/// The list of allowed path templates (['/', '/users/:id'])
|
||||
required List<String> allowedPaths,
|
||||
|
||||
/// The initial route
|
||||
String initialRoute = '/',
|
||||
|
||||
}) : initialRoute = ParsedRoute(initialRoute, initialRoute, {}, {}),
|
||||
_pathTemplates = [
|
||||
...allowedPaths,
|
||||
],
|
||||
assert(allowedPaths.contains(initialRoute));
|
||||
|
||||
@override
|
||||
Future<ParsedRoute> parseRouteInformation(
|
||||
RouteInformation routeInformation,
|
||||
) async {
|
||||
print("=======parseRouteInformation:${routeInformation.uri.path}===================");
|
||||
|
||||
final uri = routeInformation.uri;
|
||||
final path = uri.toString();
|
||||
final queryParams = uri.queryParameters;
|
||||
var parsedRoute = initialRoute;
|
||||
|
||||
for (var pathTemplate in _pathTemplates) {
|
||||
final parameters = <String>[];
|
||||
var pathRegExp = pathToRegExp(pathTemplate, parameters: parameters);
|
||||
if (pathRegExp.hasMatch(path)) {
|
||||
final match = pathRegExp.matchAsPrefix(path);
|
||||
if (match == null) continue;
|
||||
final params = extract(parameters, match);
|
||||
parsedRoute = ParsedRoute(path, pathTemplate, params, queryParams);
|
||||
}
|
||||
}
|
||||
|
||||
return parsedRoute;
|
||||
}
|
||||
|
||||
@override
|
||||
RouteInformation restoreRouteInformation(ParsedRoute configuration) {
|
||||
print("=======restoreRouteInformation:${configuration}===================");
|
||||
|
||||
return RouteInformation(uri: Uri.parse(configuration.path));
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'parsed_route.dart';
|
||||
import 'parser.dart';
|
||||
|
||||
/// The current route state. To change the current route, call obtain the state
|
||||
/// using `RouteStateScope.of(context)` and call `go()`:
|
||||
///
|
||||
/// ```
|
||||
/// RouteStateScope.of(context).go('/book/2');
|
||||
/// ```
|
||||
class RouteState extends ChangeNotifier {
|
||||
final TemplateRouteParser _parser;
|
||||
ParsedRoute _route;
|
||||
|
||||
RouteState(this._parser) : _route = _parser.initialRoute;
|
||||
|
||||
ParsedRoute get route => _route;
|
||||
|
||||
set route(ParsedRoute route) {
|
||||
// Don't notify listeners if the path hasn't changed.
|
||||
if (_route == route) return;
|
||||
|
||||
_route = route;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> go(String route) async {
|
||||
this.route = await _parser
|
||||
.parseRouteInformation(RouteInformation(uri: Uri.parse(route)));
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides the current [RouteState] to descendant widgets in the tree.
|
||||
class RouteStateScope extends InheritedNotifier<RouteState> {
|
||||
const RouteStateScope({
|
||||
required super.notifier,
|
||||
required super.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
static RouteState of(BuildContext context) =>
|
||||
context.dependOnInheritedWidgetOfExactType<RouteStateScope>()!.notifier!;
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../data.dart';
|
||||
import '../routing.dart';
|
||||
import '../widgets/book_list.dart';
|
||||
|
||||
class AuthorDetailsScreen extends StatelessWidget {
|
||||
final Author author;
|
||||
|
||||
const AuthorDetailsScreen({
|
||||
super.key,
|
||||
required this.author,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(author.name),
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: BookList(
|
||||
books: author.books,
|
||||
onTap: (book) {
|
||||
RouteStateScope.of(context).go('/book/${book.id}');
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../data/library.dart';
|
||||
import '../routing.dart';
|
||||
import '../widgets/author_list.dart';
|
||||
|
||||
class AuthorsScreen extends StatelessWidget {
|
||||
final String title = 'Authors';
|
||||
|
||||
const AuthorsScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(title),
|
||||
),
|
||||
body: AuthorList(
|
||||
authors: libraryInstance.allAuthors,
|
||||
onTap: (author) {
|
||||
RouteStateScope.of(context).go('/author/${author.id}');
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/link.dart';
|
||||
|
||||
import '../data.dart';
|
||||
import 'author_details.dart';
|
||||
|
||||
class BookDetailsScreen extends StatelessWidget {
|
||||
final Book? book;
|
||||
|
||||
const BookDetailsScreen({
|
||||
super.key,
|
||||
this.book,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (book == null) {
|
||||
return const Scaffold(
|
||||
body: Center(
|
||||
child: Text('No book found.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(book!.title),
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
book!.title,
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
Text(
|
||||
book!.author.name,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
TextButton(
|
||||
child: const Text('View author (Push)'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).push<void>(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) =>
|
||||
AuthorDetailsScreen(author: book!.author),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Link(
|
||||
uri: Uri.parse('/author/${book!.author.id}'),
|
||||
builder: (context, followLink) => TextButton(
|
||||
onPressed: followLink,
|
||||
child: const Text('View author (Link)'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../data.dart';
|
||||
import '../routing.dart';
|
||||
import '../widgets/book_list.dart';
|
||||
|
||||
class BooksScreen extends StatefulWidget {
|
||||
const BooksScreen({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<BooksScreen> createState() => _BooksScreenState();
|
||||
}
|
||||
|
||||
class _BooksScreenState extends State<BooksScreen>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 3, vsync: this)
|
||||
..addListener(_handleTabIndexChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
|
||||
final newPath = _routeState.route.pathTemplate;
|
||||
if (newPath.startsWith('/books/popular')) {
|
||||
_tabController.index = 0;
|
||||
} else if (newPath.startsWith('/books/new')) {
|
||||
_tabController.index = 1;
|
||||
} else if (newPath == '/books/all') {
|
||||
_tabController.index = 2;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.removeListener(_handleTabIndexChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Books'),
|
||||
elevation: 8,
|
||||
bottom: TabBar(
|
||||
controller: _tabController,
|
||||
tabs: const [
|
||||
Tab(
|
||||
text: 'Popular',
|
||||
icon: Icon(Icons.people),
|
||||
),
|
||||
Tab(
|
||||
text: 'New',
|
||||
icon: Icon(Icons.new_releases),
|
||||
),
|
||||
Tab(
|
||||
text: 'All',
|
||||
icon: Icon(Icons.list),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
BookList(
|
||||
books: libraryInstance.popularBooks,
|
||||
onTap: _handleBookTapped,
|
||||
),
|
||||
BookList(
|
||||
books: libraryInstance.newBooks,
|
||||
onTap: _handleBookTapped,
|
||||
),
|
||||
BookList(
|
||||
books: libraryInstance.allBooks,
|
||||
onTap: _handleBookTapped,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
RouteState get _routeState => RouteStateScope.of(context);
|
||||
|
||||
void _handleBookTapped(Book book) {
|
||||
_routeState.go('/book/${book.id}');
|
||||
}
|
||||
|
||||
void _handleTabIndexChanged() {
|
||||
switch (_tabController.index) {
|
||||
case 1:
|
||||
_routeState.go('/books/new');
|
||||
case 2:
|
||||
_routeState.go('/books/all');
|
||||
case 0:
|
||||
default:
|
||||
_routeState.go('/books/popular');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../auth.dart';
|
||||
import '../data.dart';
|
||||
import '../routing.dart';
|
||||
import '../screens/sign_in.dart';
|
||||
import '../widgets/fade_transition_page.dart';
|
||||
import 'author_details.dart';
|
||||
import 'book_details.dart';
|
||||
import 'scaffold.dart';
|
||||
|
||||
/// Builds the top-level navigator for the app. The pages to display are based
|
||||
/// on the `routeState` that was parsed by the TemplateRouteParser.
|
||||
class BookstoreNavigator extends StatefulWidget {
|
||||
final GlobalKey<NavigatorState> navigatorKey;
|
||||
|
||||
const BookstoreNavigator({
|
||||
required this.navigatorKey,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<BookstoreNavigator> createState() => _BookstoreNavigatorState();
|
||||
}
|
||||
|
||||
class _BookstoreNavigatorState extends State<BookstoreNavigator> {
|
||||
final _signInKey = const ValueKey('Sign in');
|
||||
final _scaffoldKey = const ValueKey('App scaffold');
|
||||
final _bookDetailsKey = const ValueKey('Book details screen');
|
||||
final _authorDetailsKey = const ValueKey('Author details screen');
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final routeState = RouteStateScope.of(context);
|
||||
final pathTemplate = routeState.route.pathTemplate;
|
||||
|
||||
Book? selectedBook;
|
||||
if (pathTemplate == '/book/:bookId') {
|
||||
selectedBook = libraryInstance.allBooks.firstWhereOrNull(
|
||||
(b) => b.id.toString() == routeState.route.parameters['bookId']);
|
||||
}
|
||||
|
||||
Author? selectedAuthor;
|
||||
if (pathTemplate == '/author/:authorId') {
|
||||
selectedAuthor = libraryInstance.allAuthors.firstWhereOrNull(
|
||||
(b) => b.id.toString() == routeState.route.parameters['authorId']);
|
||||
}
|
||||
|
||||
return Navigator(
|
||||
key: widget.navigatorKey,
|
||||
onPopPage: (route, dynamic result) {
|
||||
// When a page that is stacked on top of the scaffold is popped, display
|
||||
// the /books or /authors tab in BookstoreScaffold.
|
||||
if (route.settings is Page &&
|
||||
(route.settings as Page).key == _bookDetailsKey) {
|
||||
routeState.go('/books/popular');
|
||||
}
|
||||
|
||||
if (route.settings is Page &&
|
||||
(route.settings as Page).key == _authorDetailsKey) {
|
||||
routeState.go('/authors');
|
||||
}
|
||||
|
||||
return route.didPop(result);
|
||||
},
|
||||
pages: [
|
||||
if (routeState.route.pathTemplate == '/signin')
|
||||
// Display the sign in screen.
|
||||
FadeTransitionPage<void>(
|
||||
key: _signInKey,
|
||||
child: SignInScreen(
|
||||
onSignIn: (credentials) async {
|
||||
await routeState.go('/books/popular');
|
||||
},
|
||||
),
|
||||
)
|
||||
else ...[
|
||||
// Display the app
|
||||
FadeTransitionPage<void>(
|
||||
key: _scaffoldKey,
|
||||
child: const BookstoreScaffold(),
|
||||
),
|
||||
// Add an additional page to the stack if the user is viewing a book
|
||||
// or an author
|
||||
if (selectedBook != null)
|
||||
MaterialPage<void>(
|
||||
key: _bookDetailsKey,
|
||||
child: BookDetailsScreen(
|
||||
book: selectedBook,
|
||||
),
|
||||
)
|
||||
else if (selectedAuthor != null)
|
||||
MaterialPage<void>(
|
||||
key: _authorDetailsKey,
|
||||
child: AuthorDetailsScreen(
|
||||
author: selectedAuthor,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
import 'package:adaptive_navigation/adaptive_navigation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../routing.dart';
|
||||
import 'scaffold_body.dart';
|
||||
|
||||
class BookstoreScaffold extends StatelessWidget {
|
||||
const BookstoreScaffold({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final routeState = RouteStateScope.of(context);
|
||||
final selectedIndex = _getSelectedIndex(routeState.route.pathTemplate);
|
||||
|
||||
return Scaffold(
|
||||
body: AdaptiveNavigationScaffold(
|
||||
selectedIndex: selectedIndex,
|
||||
body: const BookstoreScaffoldBody(),
|
||||
onDestinationSelected: (idx) {
|
||||
if (idx == 0) routeState.go('/books/popular');
|
||||
if (idx == 1) routeState.go('/authors');
|
||||
if (idx == 2) routeState.go('/settings');
|
||||
},
|
||||
destinations: const [
|
||||
AdaptiveScaffoldDestination(
|
||||
title: 'Books',
|
||||
icon: Icons.book,
|
||||
),
|
||||
AdaptiveScaffoldDestination(
|
||||
title: 'Authors',
|
||||
icon: Icons.person,
|
||||
),
|
||||
AdaptiveScaffoldDestination(
|
||||
title: 'Settings',
|
||||
icon: Icons.settings,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
int _getSelectedIndex(String pathTemplate) {
|
||||
if (pathTemplate.startsWith('/books')) return 0;
|
||||
if (pathTemplate == '/authors') return 1;
|
||||
if (pathTemplate == '/settings') return 2;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user