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() 和 Flutter Router 小部件

目录

术语说明

看一下以下与URL路径 home/book;id=2 相关的术语:

  • 字符串路径:例如 home/book;id=2
  • 字符串段:字符串路径由两个斜杠分隔的字符串段组成(homebook;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);
}
encodedecode 帮助将 类型化段 转换为 字符串段 并返回。

步骤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 - 编码屏幕小部件

需要编写两个屏幕:HomeScreenBookScreen。扩展这些屏幕以使用 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, getDateTimeNullsetDateTime 在你的 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

1 回复

更多关于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'),
      ),
    );
  }
}
回到顶部