This commit is contained in:
toly
2023-11-13 16:15:49 +08:00
parent 5396712cf9
commit 8de38299e2
57 changed files with 3039 additions and 156 deletions

View File

@@ -0,0 +1,178 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'views/navigator_scope.dart';
import 'iroute.dart';
import 'iroute_config.dart';
import 'route_history_manager.dart';
import 'routes.dart';
import 'views/not_find_view.dart';
AppRouterDelegate router = AppRouterDelegate(
initial: IRouteConfig(
uri: Uri.parse('/app/color'),
),
node: appRoute);
class AppRouterDelegate extends RouterDelegate<IRouteConfig> with ChangeNotifier {
/// 核心数据,路由配置数据列表
final List<IRouteConfig> _configs = [];
String get path => current.uri.path;
IRouteConfig get current => _configs.last;
final IRoutePageBuilder? notFindPageBuilder;
final IRouteNode node;
@override
IRouteConfig get currentConfiguration => current;
AppRouterDelegate({
this.notFindPageBuilder,
required this.node,
required IRouteConfig initial,
}) {
_configs.add(initial);
_historyManager.recode(initial);
}
Page _defaultNotFindPageBuilder(_, __) => const MaterialPage(
child: Material(child: NotFindPage()),
);
final RouteHistoryManager _historyManager = RouteHistoryManager();
RouteHistoryManager get historyManager => _historyManager;
/// 历史回退操作
/// 详见: [RouteHistoryManager.back]
void back() => _historyManager.back(changeRoute);
/// 撤销回退操作
/// 详见: [RouteHistoryManager.revocation]
void revocation() => _historyManager.revocation(changeRoute);
void closeHistory(int index) {
_historyManager.close(index);
notifyListeners();
}
void clearHistory() {
_historyManager.clear();
notifyListeners();
}
// final List<IRouteConfig> _pathStack = [];
bool get canPop => _configs.where((e) => e.routeStyle == RouteStyle.push).isNotEmpty;
final Map<String, Completer<dynamic>> _completerMap = {};
FutureOr<dynamic> changeRoute(IRouteConfig config) {
String value = config.uri.path;
if (current == config) null;
_handleChangeStyle(config);
if (config.forResult) {
_completerMap[value] = Completer();
}
if (config.recordHistory) {
_historyManager.recode(config);
}
notifyListeners();
if (config.forResult) {
return _completerMap[value]!.future;
}
}
void _handleChangeStyle(IRouteConfig config) {
switch (config.routeStyle) {
case RouteStyle.push:
if (_configs.contains(config)) {
_configs.remove(config);
}
_configs.add(config);
break;
case RouteStyle.replace:
List<IRouteConfig> liveRoutes =
_configs.where((e) => e.keepAlive && e != config).toList();
_configs.clear();
_configs.addAll([...liveRoutes, config]);
break;
}
}
FutureOr<dynamic> changePath(
String value, {
bool forResult = false,
Object? extra,
bool keepAlive = false,
bool recordHistory = false,
RouteStyle style = RouteStyle.replace,
}) {
return changeRoute(IRouteConfig(
uri: Uri.parse(value),
forResult: forResult,
extra: extra,
routeStyle: style,
keepAlive: keepAlive,
recordHistory: recordHistory,
));
}
@override
Widget build(BuildContext context) {
return NavigatorScope(
node: node,
onPopPage: _onPopPage,
configs: _configs,
notFindPageBuilder: (notFindPageBuilder ?? _defaultNotFindPageBuilder),
);
}
@override
Future<bool> popRoute() async {
print('=======popRoute=========');
return true;
}
void backStack() {
if (_configs.isNotEmpty) {
_configs.removeLast();
if (_configs.isNotEmpty) {
changeRoute(_configs.last);
} else {
changeRoute(current);
}
}
}
bool _onPopPage(Route route, result) {
if (_completerMap.containsKey(path)) {
_completerMap[path]?.complete(result);
_completerMap.remove(path);
}
if (canPop) {
_configs.removeLast();
notifyListeners();
} else {
changePath(backPath(path), recordHistory: false);
}
return route.didPop(result);
}
String backPath(String path) {
Uri uri = Uri.parse(path);
if (uri.pathSegments.length == 1) return path;
List<String> parts = List.of(uri.pathSegments)..removeLast();
return '/${parts.join('/')}';
}
@override
Future<void> setNewRoutePath(configuration) async {}
}

View File

@@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
import 'iroute_config.dart';
class AppRouterParser extends RouteInformationParser<IRouteConfig>{
@override
RouteInformation restoreRouteInformation(IRouteConfig configuration) {
return RouteInformation(
uri: configuration.uri
);
}
@override
Future<IRouteConfig> parseRouteInformationWithDependencies(
RouteInformation routeInformation, BuildContext context) async{
return IRouteConfig(uri: routeInformation.uri);
}
}

View File

@@ -0,0 +1,157 @@
import 'package:flutter/material.dart';
import 'package:iroute/v9/app/navigation/router/views/navigator_scope.dart';
import 'iroute_config.dart';
typedef IRoutePageBuilder = Page? Function(
BuildContext context,
IRouteConfig data,
);
typedef IRouteWidgetBuilder = Widget? Function(
BuildContext context,
IRouteConfig data,
);
abstract class IRouteNode {
final String path;
final List<IRouteNode> children;
const IRouteNode({
required this.path,
required this.children,
});
Page? createPage(BuildContext context, IRouteConfig config);
List<IRouteNode> find(
String input,
) {
String prefix = '/';
if (this is CellIRoute) {
input = input.replaceFirst(path, '');
if (path != '/') {
prefix = path + "/";
}
}
return findNodes(this, Uri.parse(input), 0, prefix, []);
}
List<IRouteNode> findNodes(
IRouteNode node,
Uri uri,
int deep,
String prefix,
List<IRouteNode> result,
) {
List<String> parts = uri.pathSegments;
if (deep > parts.length - 1) {
return result;
}
String target = parts[deep];
if (node.children.isNotEmpty) {
target = prefix + target;
List<IRouteNode> nodes =
node.children.where((e) => e.path == target).toList();
bool match = nodes.isNotEmpty;
if (match) {
IRouteNode matched = nodes.first;
result.add(matched);
String nextPrefix = '${matched.path}/';
findNodes(matched, uri, ++deep, nextPrefix, result);
} else {
result.add(NotFindNode(path: target));
return result;
}
}
return result;
}
}
/// 优先调用 [pageBuilder] 构建 Page
/// 没有 [pageBuilder] 时, 使用 [widgetBuilder] 构建组件
/// 没有 [pageBuilder] 和 [widgetBuilder] 时, 使用 [widget] 构建组件
class IRoute extends IRouteNode {
final IRoutePageBuilder? pageBuilder;
final IRouteWidgetBuilder? widgetBuilder;
final Widget? widget;
const IRoute({
required super.path,
super.children = const [],
this.widget,
this.pageBuilder,
this.widgetBuilder,
});
@override
Page? createPage(BuildContext context, IRouteConfig config) {
if (pageBuilder != null) {
return pageBuilder!(context, config);
}
Widget? child;
if (widgetBuilder != null) {
child = widgetBuilder!(context, config);
}
child ??= widget;
if (child != null) {
return MaterialPage(child: child, key: config.pageKey);
}
return null;
}
}
/// 未知路由
class NotFindNode extends IRouteNode {
NotFindNode({required super.path, super.children = const []});
@override
Page? createPage(BuildContext context, IRouteConfig config) {
return null;
}
}
typedef CellBuilder = Widget Function(
BuildContext context,
IRouteConfig config,
Widget navigator,
);
typedef CellIRoutePageBuilder = Page? Function(
BuildContext context,
IRouteConfig data,
Widget child,
);
class CellIRoute extends IRouteNode {
final CellBuilder cellBuilder;
final CellIRoutePageBuilder? pageBuilder;
const CellIRoute({
required this.cellBuilder,
this.pageBuilder,
required super.path,
required super.children,
});
@override
Page? createPage(BuildContext context, IRouteConfig config) {
return null;
}
Page? createCellPage(BuildContext context, IRouteConfig config,
Widget child) {
if (pageBuilder != null) {
return pageBuilder!(context, config, child);
}
print("======CellIRoute#createCellPage${config.pageKey}=================");
return MaterialPage(
child: child,
key: config.pageKey,
);
}
}

View File

@@ -0,0 +1,71 @@
import 'package:flutter/material.dart';
enum RouteStyle{
push,
replace,
}
class IRouteConfig {
final Object? extra;
final bool forResult;
final Uri uri;
final bool keepAlive;
final RouteStyle routeStyle;
final bool recordHistory;
const IRouteConfig({
this.extra,
required this.uri,
this.forResult = false,
this.keepAlive = false,
this.routeStyle = RouteStyle.replace,
this.recordHistory = false,
});
String get path => uri.path;
IRouteConfig copyWith({
Object? extra,
bool? forResult,
bool? keepAlive,
bool? recordHistory,
String? path,
}) =>
IRouteConfig(
extra: extra ?? this.extra,
forResult: forResult ?? this.forResult,
keepAlive: keepAlive ?? this.keepAlive,
recordHistory: recordHistory ?? this.recordHistory,
uri: path!=null?Uri.parse(path):uri,
);
ValueKey get pageKey => ValueKey(hashCode);
@override
String toString() {
return 'IRouteConfig{extra: $extra, forResult: $forResult, uri: $uri, keepAlive: $keepAlive, routeStyle: $routeStyle, recordHistory: $recordHistory}';
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is IRouteConfig &&
runtimeType == other.runtimeType &&
extra == other.extra &&
forResult == other.forResult &&
uri == other.uri &&
keepAlive == other.keepAlive &&
routeStyle == other.routeStyle &&
recordHistory == other.recordHistory;
@override
int get hashCode =>
extra.hashCode ^
forResult.hashCode ^
uri.hashCode ^
keepAlive.hashCode ^
routeStyle.hashCode ^
recordHistory.hashCode;
}

View File

@@ -0,0 +1,47 @@
import 'iroute_config.dart';
typedef OnRouteChange = void Function(IRouteConfig config);
class RouteHistoryManager{
final List<IRouteConfig> _histories = [];
final List<IRouteConfig> _backHistories = [];
List<IRouteConfig> get histories => _histories.reversed.toList();
bool get hasHistory => _histories.length > 1;
bool get hasBackHistory => _backHistories.isNotEmpty;
/// 将 [config] 加入历史记录
void recode(IRouteConfig config){
if (_histories.isNotEmpty && config.path == _histories.last.path) return;
_histories.add(config);
}
/// 历史回退操作
/// 将当前顶层移除,并加入 [_backHistories] 撤销列表
/// 并转到前一路径 [_histories.last]
void back(OnRouteChange callback) {
if (!hasHistory) return;
IRouteConfig top = _histories.removeLast();
_backHistories.add(top);
if (_histories.isNotEmpty) {
callback(_histories.last);
}
}
/// 撤销回退操作
/// 取出回退列表的最后元素,跳转到该路径
void revocation(OnRouteChange callback) {
IRouteConfig target = _backHistories.removeLast();
callback(target);
}
void close(int index) {
_histories.removeAt(index);
}
void clear() {
_histories.clear();
}
}

View File

@@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
import 'package:iroute/components/components.dart';
import '../../../pages/login/login.dart';
import '../transition/no_transition_page.dart';
import '../../../pages/sort/views/player/sort_player.dart';
import 'iroute_config.dart';
import '../views/app_navigation.dart';
import 'iroute.dart';
import '../../../pages/color/color_add_page.dart';
import '../../../pages/color/color_detail_page.dart';
import '../../../pages/color/color_page.dart';
import '../../../pages/counter/counter_page.dart';
import '../../../pages/user/user_page.dart';
import '../../../pages/settings/settings_page.dart';
import '../../../pages/sort/views/sort_page/sort_page.dart';
import '../../../pages/sort/views/settings/sort_setting.dart';
CellIRoute appRoute = CellIRoute(
cellBuilder: (_, __, navigator) => AppNavigation(
navigator: navigator,
),
path: '/app',
children: [
IRoute(
path: '/app/color',
widget: ColorPage(),
children: [
IRoute(path: '/app/color/detail', widgetBuilder: _buildColorDetail),
IRoute(path: '/app/color/add', widget: ColorAddPage()),
],
),
const IRoute(path: '/app/counter', widget: CounterPage()),
CellIRoute(
cellBuilder: (_, __, navigator) => SortNavigation(navigator: navigator),
// pageBuilder: (_,config,child)=> NoTransitionPage(
// child: child,
// key: config.pageKey
// ),
path: '/app/sort',
children: [
const IRoute(
path: '/app/sort/settings',
widget: SortSettings(),
),
const IRoute(
path: '/app/sort/player',
widget: SortPlayer(),
),
],
),
const IRoute(path: '/app/user', widget: UserPage()),
const IRoute(path: '/app/settings', widget: SettingPage()),
],
);
IRoute rootRoute = IRoute(path: '/', children: [
appRoute,
const IRoute(
path: '/login',
widget: LoginPage()
)
]);
Widget? _buildColorDetail(BuildContext context, IRouteConfig data) {
final Map<String, String> queryParams = data.uri.queryParameters;
String? selectedColor = queryParams['color'];
Color color = Colors.black;
if (selectedColor != null) {
color = Color(int.parse(selectedColor, radix: 16));
} else if (data.extra is Color) {
color = data.extra as Color;
}
return ColorDetailPage(color: color);
}
Map<String, String> kRouteLabelMap = {
'/app': '',
'/app/color': '颜色板',
'/app/color/add': '添加颜色',
'/app/color/detail': '颜色详情',
'/app/counter': '计数器',
'/app/sort': '排序算法',
'/app/sort/player': '演示',
'/app/sort/settings': '排序配置',
'/app/user': '我的',
'/app/settings': '系统设置',
};
const List<MenuMeta> deskNavBarMenus = [
MenuMeta(label: '颜色板', icon: Icons.color_lens_outlined, path: '/app/color'),
MenuMeta(label: '计数器', icon: Icons.add_chart, path: '/app/counter'),
MenuMeta(label: '排序', icon: Icons.sort, path: '/app/sort/player'),
MenuMeta(label: '我的', icon: Icons.person, path: '/app/user'),
MenuMeta(label: '设置', icon: Icons.settings, path: '/app/settings'),
];

View File

@@ -0,0 +1,109 @@
import 'package:flutter/material.dart';
import '../iroute.dart';
import '../iroute_config.dart';
class NavigatorScope extends StatefulWidget {
final IRouteNode node;
final PopPageCallback onPopPage;
final List<IRouteConfig> configs;
final IRoutePageBuilder notFindPageBuilder;
const NavigatorScope({
super.key,
required this.node,
required this.onPopPage,
required this.configs,
required this.notFindPageBuilder,
});
@override
State<NavigatorScope> createState() => _NavigatorScopeState();
}
class _NavigatorScopeState extends State<NavigatorScope> {
@override
Widget build(BuildContext context) {
Widget content = Navigator(
onPopPage: widget.onPopPage,
pages: _buildPages(context, widget.configs),
);
if(widget.node is CellIRoute){
content = (widget.node as CellIRoute).cellBuilder(context,widget.configs.last,content);
}
return HeroControllerScope(
controller: MaterialApp.createMaterialHeroController(),
child: content,
);
}
List<Page> _buildPages(BuildContext context, List<IRouteConfig> configs) {
IRouteConfig top = configs.last;
List<IRouteConfig> bottoms =
configs.sublist(0, configs.length - 1).toList();
List<Page> pages = [];
List<Page> topPages = _buildPageByPathFromTree(context, top);
pages = _buildLivePageByPathList(context, bottoms, top, topPages);
pages.addAll(topPages);
return pages;
}
List<Page> _buildLivePageByPathList(
BuildContext context,
List<IRouteConfig> paths,
IRouteConfig curConfig,
List<Page> curPages,
) {
List<Page> pages = [];
if (paths.isNotEmpty) {
for (IRouteConfig path in paths) {
if (path != curConfig) {
pages.addAll(_buildPageByPathFromTree(context, path));
}
}
/// 去除和 curPages 中重复的界面
pages.removeWhere((page) => curPages.map((e) => e.key).contains(page.key));
}
return pages;
}
List<Page> _buildPageByPathFromTree(
BuildContext context, IRouteConfig config) {
List<Page> result = [];
List<IRouteNode> iRoutes = widget.node.find(config.path);
if (iRoutes.isNotEmpty) {
for (int i = 0; i < iRoutes.length; i++) {
IRouteNode iroute = iRoutes[i];
IRouteConfig fixConfig = config;
if(iroute.path!=config.uri.path){
fixConfig = IRouteConfig(uri: Uri.parse(iroute.path));
}
Page? page;
if (iroute is NotFindNode) {
page = widget.notFindPageBuilder(context, config);
} else if (iroute is CellIRoute) {
Widget scope = NavigatorScope(
node: iroute,
onPopPage: widget.onPopPage,
configs: widget.configs,
notFindPageBuilder: widget.notFindPageBuilder,
);
page = iroute.createCellPage(context, fixConfig, scope);
} else {
page = iroute.createPage(context, fixConfig);
}
if (page != null) {
result.add(page);
}
if (iroute is CellIRoute) {
break;
}
}
}
return result;
}
}

View File

@@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
class NotFindPage extends StatelessWidget {
const NotFindPage({super.key});
@override
Widget build(BuildContext context) {
return const Material(
child: Center(
child: Wrap(
spacing: 16,
crossAxisAlignment: WrapCrossAlignment.center,
direction: Axis.vertical,
children: [
Icon(Icons.nearby_error,size: 64, color: Colors.redAccent),
Text(
'404 Page Not Find',
style: TextStyle(fontSize: 24, color: Colors.grey),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import '../app_router_delegate.dart';
class RouteBackIndicator extends StatefulWidget {
const RouteBackIndicator({super.key});
@override
State<RouteBackIndicator> createState() => _RouteBackIndicatorState();
}
class _RouteBackIndicatorState extends State<RouteBackIndicator> {
@override
void initState() {
super.initState();
router.addListener(_onChange);
}
@override
void dispose() {
router.removeListener(_onChange);
super.dispose();
}
@override
Widget build(BuildContext context) {
if(router.canPop){
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: router.backStack,
child: Container(
width: 26,
height: 26,
margin: EdgeInsets.only(right: 8),
alignment: Alignment.center,
decoration: BoxDecoration(
color: Color(0xffE3E5E7),
borderRadius: BorderRadius.circular(6)
),
child: Icon(Icons.arrow_back_ios_new,size: 14,)),
),
);
}
return SizedBox();
}
void _onChange() {
setState(() {
});
}
}