Flutter路由管理插件riverpod_navigator的使用
Flutter路由管理插件riverpod_navigator的使用
如果你对这个包的动机感兴趣,并想详细了解它解决的问题,请阅读这篇文章: Simple Flutter导航与Riverpod。
简单但强大的Flutter导航与Riverpod及Navigator 2.0结合,解决以下问题:
- 严格的类型化导航:你可以使用
navigate([HomeSegment(), BookSegment(id: 2)])
而不是在代码中使用navigate('home/book;id:2')
- 异步导航:当更改导航状态需要异步操作(如从互联网加载或保存数据)时
- 多个提供商:当导航状态依赖于多个Riverpod提供者时
- 更简单的编码:导航问题简化为操作类集合
- 更好的关注分离:UI x 模型(感谢Riverpod):导航逻辑可以在不输入任何Flutter小部件的情况下开发和测试
- 嵌套导航:只需使用嵌套的Riverpod
ProviderScope()
和 FlutterRouter
小部件
目录
术语说明
看一下以下与URL路径 home/book;id=2
相关的术语:
- 字符串路径:例如
home/book;id=2
- 字符串段:字符串路径由两个斜杠分隔的字符串段组成(
home
和book;id=2
) - 类型化段:描述相应的字符串段(
HomeSegment()
对应 ‘home’,BookSegment(id:2)
对应 ‘book;id=2’)类型化段是class TypedSegment {}
的子类。 - 类型化路径:描述相应的字符串路径(
[HomeSegment(), BookSegment(id:2)]
)类型化路径是typedef TypedPath = List<TypedSegment>;
- Flutter Navigator 2.0 的导航栈由类型化路径唯一确定(每个类型化路径的类型化段实例对应一个屏幕和页面实例):
pages = [ MaterialPage (child: HomeScreen(HomeSegment())), MaterialPage (child: BookScreen(BookSegment(id:2))) ]
简单示例
创建应用程序可以遵循以下简单步骤:
步骤1 - 定义类型化的段类
class HomeSegment extends TypedSegment {
const HomeSegment();
factory HomeSegment.decode(UrlPars pars) => const HomeSegment();
}
class BookSegment extends TypedSegment {
const BookSegment({required this.id});
factory BookSegment.decode(UrlPars pars) => BookSegment(id: pars.getInt('id'));
final int id;
@override
void encode(UrlPars pars) => pars.setInt('id', id);
}
encode 和 decode 帮助将 类型化段 转换为 字符串段 并返回。
步骤2 - 配置AppNavigator
通过扩展RNavigator类来配置AppNavigator。
class AppNavigator extends RNavigator {
AppNavigator(Ref ref)
: super(
ref,
[
/// 'home' 和 'book' 字符串用于web URL,例如 'home/book;id=2'
/// decode 用于解码URL到 HomeSegment/BookSegment
/// HomeScreen/BookScreen.new 是给定段的屏幕构建器
RRoute<HomeSegment>(
'home',
HomeSegment.decode,
HomeScreen.new,
),
RRoute<BookSegment>(
'book',
BookSegment.decode,
BookScreen.new,
),
],
);
}
步骤3 - 在MaterialApp.router中使用AppNavigator
如果你熟悉Flutter Navigator 2.0 和 Riverpod,以下代码应该很清楚:
class App extends ConsumerWidget {
const App({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
final navigator = ref.read(navigatorProvider) as AppNavigator;
return MaterialApp.router(
title: 'Riverpod Navigator Example',
routerDelegate: navigator.routerDelegate,
routeInformationParser: navigator.routeInformationParser,
);
}
}
步骤4 - 配置Riverpod ProviderScope
在主入口点配置Riverpod ProviderScope。
void main() => runApp(
ProviderScope(
// [HomeSegment()] 作为 home 类型化路径和 navigator 构造函数是必需的
overrides: riverpodNavigatorOverrides([HomeSegment()], AppNavigator.new),
child: const App(),
),
);
步骤5 - 编码屏幕小部件
需要编写两个屏幕:HomeScreen
和 BookScreen
。扩展这些屏幕以使用 RScreen
小部件。
class BookScreen extends RScreen<AppNavigator, BookSegment> {
const BookScreen(BookSegment segment) : super(segment);
@override
Widget buildScreen(ref, navigator, appBarLeading) => Scaffold(
appBar: AppBar(
title: Text('Book ${segment.id}'),
/// [appBarLeading] 替换标准返回按钮行为
leading: appBarLeading,
),
body:
...
RScreen 小部件: - 替换标准Android返回按钮行为(使用Flutter BackButtonListener小部件) - 将提供 appBarLeading 图标以替换标准AppBar返回按钮行为
这就是全部了
查看:
在 [运行示例](https://pavelpz.github.io/doc_simple/) 中的链接 “Go to book: [3, 13, 103]” 在真实的书籍应用中可能没有什么意义。但它展示了四屏导航堆栈的导航: - **字符串路径** = `home/book;id=3/book;id=13/book;id=103`。 - **类型化路径** = `[HomeSegment(), BookSegment(id:3), BookSegment(id:13), BookSegment(id:103)]`。 - **导航堆栈**(flutter Navigator.pages)= `[MaterialPage (child: HomeScreen(HomeSegment())), MaterialPage (child: BookScreen(BookSegment(id:3))), MaterialPage (child: BookScreen(BookSegment(id:13))), MaterialPage (child: BookScreen(BookSegment(id:103)))]`。
无GUI的开发和测试
导航逻辑可以在不输入任何Flutter小部件的情况下进行开发和测试:
test('navigation model', () async {
final container = ProviderContainer(
overrides: riverpodNavigatorOverrides([HomeSegment()], AppNavigator.new),
);
final navigator = container.read(navigatorProvider);
Future navigTest(Future action(), String expected) async {
await action();
await container.pump();
expect(navigator.navigationStack2Url, expected);
}
await navigTest(
() => navigator.navigate([HomeSegment(), BookSegment(id: 1)]),
'home/book;id=1',
);
await navigTest(
() => navigator.pop(),
'home',
);
await navigTest(
() => navigator.push(BookSegment(id: 2)),
'home/book;id=2',
);
await navigTest(
() => navigator.replaceLast<BookSegment>((old) => BookSegment(id: old.id + 1)),
'home/book;id=3',
);
});
URL解析
Flutter Navigator 2.0 及其 `MaterialApp.router` 构造函数需要一个URL解析器(`RouteInformationParser`)。我们使用URL语法,参见 [RFC 3986](https://www.ietf.org/rfc/rfc3986.txt) 的第3.3节,注意 *例如,一个URI生产者可能会使用一个片段,如 "name;v=1.1"...*
每个 TypedSegment
必须被转换为 string-segment
并返回。string-segment
的格式为 <unique TypedSegment id>[;<property name>=<property value>]*
,例如 book;id=3
。
编码/解码示例
而不是直接将字符串转换为/从字符串,我们将其转换为/从
typedef UrlPars = Map<String,String>;
到目前为止,我们支持以下类型的 TypedSegment
属性:
int, double, bool, String, int?, double?, bool?, String?
class TestSegment extends TypedSegment {
const TestSegment({required this.i, this.s, required this.b, this.d});
factory TestSegment.decode(UrlPars pars) => TestSegment(
i: pars.getInt('i'),
s: pars.getStringNull('s'),
b: pars.getBool('b'),
d: pars.getDoubleNull('d'),
);
@override
void encode(UrlPars pars) =>
pars.setInt('i', i).setString('s', s).setBool('b', b).setDouble('d', d);
final int i;
final String? s;
final bool b;
final double? d;
}
注册 TestSegment
后,以下URL是正确的:
test;i=1;b=true
test;i=2;b=true;d=12.6;s=abcd
test;i=2;b=true/test;i=2;b=true;d=12.6;s=abcd/test;i=3;b=false
自定义
URL转换的每个方面都可以自定义,例如:
- 支持另一种属性类型(如
DateTime
,提供getDateTime
,getDateTimeNull
和setDateTime
在你的UrlPars
扩展中) - 重写整个
IPathParser
并使用完全不同的URL语法。然后在AppNavigator
中使用你的解析器:
class AppNavigator extends RNavigator {
AppNavigator(Ref ref)
: super(
....
pathParserCreator: (router) => MyPathParser(router),
...
将导航事件放置在AppNavigator中
将所有特定于导航的代码(事件)放在 AppNavigator
中是一个好习惯。这不仅可以用于编写屏幕小部件,还可以用于测试。
class AppNavigator extends RNavigator {
......
/// 导航到下一个书籍
Future toNextBook() => replaceLast<BookSegment>((last) => BookSegment(id: last.id + 1));
/// 导航到首页
Future toHome() => navigate([HomeSegment()]);
}
在屏幕小部件中使用如下:
...
ElevatedButton(
onPressed: navigator.toNextBook,
child: Text('Book $id'),
),
...
在测试代码中使用如下:
await navigTest(navigator.toNextBook, 'home/book;id=3');
异步导航
异步导航意味着导航将延迟直到异步操作完成。每个屏幕的这些操作包括:
- 打开(在打开新屏幕之前)
- 关闭(在关闭旧屏幕之前)
- 替换(在用相同段类型的屏幕替换屏幕之前)
打开 和 关闭 操作可以返回一个异步结果,该结果稍后可用于构建新屏幕。
定义类型化的段类
为 TypedSegment
应用适当的类型(String
)的 AsyncSegment
混入。
class HomeSegment extends TypedSegment with AsyncSegment<String>{
....
}
class BookSegment extends TypedSegment with AsyncSegment<String>{
....
}
配置AppNavigator
在 RRoute
定义中添加 打开、关闭 或 替换 动作。
class AppNavigator extends RNavigator {
AppNavigator(Ref ref)
: super(
ref,
[
RRoute<HomeSegment>(
'home',
HomeSegment.decode,
HomeScreen.new,
opening: (sNew) => sNew.setAsyncValue(_simulateAsyncResult('Home.opening', 2000)),
),
RRoute<BookSegment>(
'book',
BookSegment.decode,
BookScreen.new,
opening: (sNew) => sNew.setAsyncValue(_simulateAsyncResult('Book ${sNew.id}.opening', 240)),
replacing: (sOld, sNew) => sNew.setAsyncValue(_simulateAsyncResult('Book ${sOld.id}=>${sNew.id}.replacing', 800)),
closing: (sOld) => Future.delayed(Duration(milliseconds: 500)),
),
],
);
....
}
// 模拟保存到/从外部存储的操作
Future<String> _simulateAsyncResult(String asyncResult, int msec) async {
await Future.delayed(Duration(milliseconds: msec));
return '$asyncResult: 异步结果在 $msec 毫秒后';
}
使用异步动作的结果构建屏幕
...
Text('异步结果: "${segment.asyncValue}"'),
...
更多关于Flutter路由管理插件riverpod_navigator的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter路由管理插件riverpod_navigator的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
riverpod_navigator
是一个基于 Riverpod 的 Flutter 路由管理插件,它提供了声明式的路由管理方式,使得在 Flutter 应用中管理导航变得更加简单和直观。以下是如何使用 riverpod_navigator
的基本步骤和示例。
1. 添加依赖
首先,在你的 pubspec.yaml
文件中添加 riverpod_navigator
依赖:
dependencies:
flutter:
sdk: flutter
riverpod: ^2.0.0
riverpod_navigator: ^1.0.0
然后运行 flutter pub get
来获取依赖。
2. 创建路由配置
接下来,你需要定义你的路由配置。riverpod_navigator
使用 RouteConfiguration
来定义路由。
import 'package:flutter/material.dart';
import 'package:riverpod_navigator/riverpod_navigator.dart';
final routeConfiguration = RouteConfiguration(
routes: [
RouteDefinition(
path: '/',
builder: (context) => HomePage(),
),
RouteDefinition(
path: '/details',
builder: (context) => DetailsPage(),
),
],
);
3. 创建导航器提供者
创建一个 Provider
来提供 Navigator
实例。
import 'package:flutter_riverpod/flutter_riverpod.dart';
final navigatorProvider = Provider<Navigator>(
(ref) => Navigator(
routeConfiguration: routeConfiguration,
),
);
4. 在应用中使用导航器
在你的应用中使用 Navigator
来管理路由。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_navigator/riverpod_navigator.dart';
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
[@override](/user/override)
Widget build(BuildContext context) {
return MaterialApp(
home: Consumer(
builder: (context, watch, child) {
final navigator = watch(navigatorProvider);
return navigator;
},
),
);
}
}
5. 导航到其他页面
你可以使用 context.read(navigatorProvider).pushNamed('/details')
来导航到其他页面。
class HomePage extends StatelessWidget {
[@override](/user/override)
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home Page'),
),
body: Center(
child: ElevatedButton(
onPressed: () {
context.read(navigatorProvider).pushNamed('/details');
},
child: Text('Go to Details'),
),
),
);
}
}
class DetailsPage extends StatelessWidget {
[@override](/user/override)
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Details Page'),
),
body: Center(
child: ElevatedButton(
onPressed: () {
context.read(navigatorProvider).pop();
},
child: Text('Go Back'),
),
),
);
}
}
6. 处理参数
你还可以在导航时传递参数。
context.read(navigatorProvider).pushNamed('/details', arguments: {'id': 123});
在目标页面中,你可以通过 ModalRoute.of(context)?.settings.arguments
来获取参数。
class DetailsPage extends StatelessWidget {
[@override](/user/override)
Widget build(BuildContext context) {
final arguments = ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?;
final id = arguments?['id'];
return Scaffold(
appBar: AppBar(
title: Text('Details Page'),
),
body: Center(
child: Text('ID: $id'),
),
);
}
}