Flutter Navigator 2.0 Web Param (2)

介绍
在本指南的第 1 部分中,我们将一个简单的 2 屏应用程序迁移到了 Navigator 2.0。为了保持简单,我们做了最低限度的升级,从 Navigator 1.0 升级,只创建一个 RouterDelegate,然后用它代替 Navigator 来发出导航请求。

但是,这种方法错过了 Navigator 2.0 的一些关键优势。例如,能够将地址栏中的 URL 同步到应用程序的当前位置。这也防止用户能够输入特定的 URL 或点击链接到我们应用程序中的特定页面,如下所示。

跳转url不带参数

这显然不是理想的网络体验。因此,我们将更新我们的应用程序,进一步利用 Navigator 2.0 为我们的用户优化网络体验。

这个项目的所有代码都可以在这里找到。我鼓励您下载并继续学习。

升级我们的实施
路由代理
第一步是向我们的 RouterDelegate 添加一些功能。我们将实现 currentConfiguration getter。这是 URL 同步所必需的,默认情况下返回 null。我们将返回当前页面列表的副本。请注意,即使我们的 RouterDelegate 是 List<RouteSettings> 类型,我们也可以返回一个 List<Page>。正如在第 1 部分中讨论的那样,Page 扩展了 RouteSettings,因此效果很好。

@override
List<Page> get currentConfiguration => List.of(_pages);

由于我们将允许用户通过在地址栏中输入 URL 进行导航,因此我们必须考虑不对应于我们应用程序中任何位置的 URL (404)。让我们更新我们的 _createPage 方法以默认返回一个简单的 404 错误页面。

MaterialPage _createPage(RouteSettings routeSettings) {
Widget child;
switch (routeSettings.name) {
case '/':
child = HomePage();
break;
case '/recipe':
child = RecipePage(routeSettings.arguments);
break;
default:
child = Scaffold(
appBar: AppBar(title: Text('404')),
body: Center(child: Text('Page not found')),
);
}
return MaterialPage(
child: child,
key: Key(routeSettings.name),
name: routeSettings.name,
arguments: routeSettings.arguments,
);
}

现在我们将允许用户通过在地址栏中输入 URL 来设置路由路径,我们必须在 RouterDelegates setNewRoutePath 方法中处理它。您可能记得我们需要覆盖它,但在第 1 部分中将其留空。让我们更新它以从 RouteSettings 列表构建我们的 RouterDelegate 的页面列表。我们将使用一个名为 _setPath 的辅助方法来执行此操作。请注意,如果 HomePage 不存在,我们会将其插入到列表的开头。这是由于路径的解析方式。

@override
Future<void> setNewRoutePath(List<RouteSettings> configuration) {
_setPath(configuration
.map((routeSettings) => _createPage(routeSettings)
.toList());
return Future.value(null);
}
void _setPath(List<Page> pages) {
_pages.clear();
_pages.addAll(pages);
if (_pages.first.name != '/')
_pages.insert(0, _createPage(RouteSettings(name: '/')));
notifyListeners();
}

路由信息解析器
现在我们需要扩展 RouteInformationParser。此类负责将 URL 与我们的应用程序同步。在路由器目录中创建文件 information_parser.dart 并定义 MyRouteInformationParser。定义一个常量。构造函数,因为我们可以而且很好。

class MyRouteInformationParser extends
RouteInformationParser<List<RouteSettings>> {
const MyRouteInformationParser() : super();
@override
Future<List<RouteSettings>> parseRouteInformation(
RouteInformation routeInformation) {
// TODO: implement parseRouteInformation
throw UnimplementedError();
}
@override
RouteInformation restoreRouteInformation(configuration) {
// TODO: implement restoreRouteInformation
return super.restoreRouteInformation(configuration);
}
}

parseRouteInformation 帮助我们将 URL 转换为应用位置,restoreRouteInformation 帮助我们将路由位置转换为 URL。

首先,我们覆盖 parseRouteInformation 以将位置解析为 URI。然后我们检查路径段。如果为空,我们将路由到主页。否则,我们将这些段映射到 RouteSettings 列表,将 URL 中的任何查询参数作为参数传递给我们列表中的最后一个 RouteSettings。最后,我们返回 RouteSettingsList 的 Future.value。

@override
Future<List<RouteSettings>> parseRouteInformation(
RouteInformation routeInformation) {
final uri = Uri.parse(routeInformation.location);
if (uri.pathSegments.isEmpty)
return Future.value([RouteSettings(name: '/')]);
final routeSettings = uri.pathSegments
.map((pathSegment) => RouteSettings(
name: '/$pathSegment',
arguments: pathSegment == uri.pathSegments.last
? uri.queryParameters
: null,
))
.toList();
return Future.value(routeSettings);
}

接下来,我们覆盖 restoreRouteInformation 以从我们的 RotuerDelegates 当前配置中识别一个位置。 (还记得我们之前添加的 getter 吗?)然后我们返回一个包含该位置的 RouterInformation 对象。如果我们在接受参数/参数的页面上,我们还将添加一个辅助方法,用于将我们的路由参数转换为 URL 查询参数。我们处理的唯一参数/参数当然是recipe路由上的recipe id。

@override
RouteInformation restoreRouteInformation(List<RouteSettings>
configuration) {
final location = configuration.last.name;
final arguments = _restoreArguments(configuration.last);
return RouteInformation(location: 'locationarguments');
}
String _restoreArguments(RouteSettings routeSettings) {
if (routeSettings.name != '/recipe') return '';
return '?id=${routeSettings.arguments}';
}

当然,URL 路径结构是一种设计选择,您的可能会有所不同。但是这些方法中应用的原理保持不变。相应地调整。

因为我们的查询参数将采用 Map<String, String> 的形式,所以我们可以从将路由参数更新为相同的形式中受益。我们以前只在屏幕之间传递食谱 ID。如果我们在 parseRouteInformation 中提取 id,这仍然有效,但通过使用 Map,我们实际上将使我们的应用程序更加健壮和面向未来。

首先,我们应该更新 _restoreArguments 方法以从 Map 中提取 id。

return '?id=${(routeSettings.arguments as Map)['id'].toString()}';

现在我们需要更新 RecipePage 的变量名称以反映我们的更改并更新它在 Api.fetchRecipe 调用中的使用。

// RecipePage
final _arguments;
RecipePage(this._arguments);
// _RecipePageState
@override
void initState() {
Api.fetchRecipe(widget._arguments['id'])
.then((cocktail) => setState(() => _cocktail = cocktail));
super.initState();
}

最后,我们更新 _HomePageState 中的 routerDelegate.pushPage 调用以传递一个 Map 作为参数。

onTap: () => routerDelegate.pushPage(
name: '/recipe',
arguments: {
'id': _cocktails[index].id,
},
),

现在转到 MyApp 小部件。更新路由器以使用我们的 RouteInformationParser。

@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Navigator 2.0 Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Router(
routerDelegate: routerDelegate,
routeInformationParser: const MyRouteInformationParser(),
backButtonDispatcher: RootBackButtonDispatcher(),
),
);
}

注意:我们现在可以使用 MaterialApp.router 构造函数。这将允许我们避免定义 backButtonDispatcher 并稍微清理我们的代码。我们之前无法使用它,因为此构造函数需要一个非空的 RouteInformationParser,尽管如果您不需要同步 URL,Router 实际上并不需要一个。让我们更新我们的构建方法以利用它。

@override
Widget build(BuildContext context) {
return MaterialApp.router(
debugShowCheckedModeBanner: false,
title: 'Navigator 2.0 Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
routerDelegate: routerDelegate,
routeInformationParser: const MyRouteInformationParser(),
);
}

处理 404 错误
就像我们对 404 页面请求的考虑一样,我们必须考虑用户输入无效的食谱 ID 作为查询参数。首先,让我们在 Cocktail 类中定义一个 Cocktail.notFound 构造函数。

Cocktail.notFound()
: id = '-1',
name = '404';

现在在我们的 Api.fetchRecipe 方法中,如果我们从 TheCocktailDB API 获得空响应,我们将返回 Cocktail.notFound() 。在当前 return 语句的正上方添加以下行。
if (cocktail == null) return Cocktail.notFound();

最后,我们将更新我们的 _RecipePageState 的构建方法来处理带有 id == '-1' 的鸡尾酒。将以下 appBar 参数添加到 Scaffold。

appBar: _cocktail != null && _cocktail.id == '-1'
? AppBar(
title: Text('404'),
)
: null,

让我们也更新正文以显示未找到食谱。在 _cocktail != null ? 和 CustomScrollView 之间添加以下内容。

_cocktail.id == '-1'
? Center(child: Text('Recipe not found.'))

差不多了
让我们看看到目前为止我们的劳动成果。

image.png

那么这里发生了什么? RouterDelegate 告诉 Navigator 何时通过侦听我们的页面列表中的更改来重建。它知道发生变化的唯一方法是我们列表中给定索引处的 Page 的键是否发生变化。回顾我们的 _createPage 方法,我们看到我们将路由名称指定为 MaterialPage 的键。在添加、删除甚至换出页面时,这非常有效。然而,当我们在更新参数时保持相同的页面不变时,Flutter 无法知道它需要用这个新信息重建 Widget。为了解决这个问题,我们将为键分配一个同时考虑名称和参数的值。 RouteSettings 的 String 表示将很好地用于此。

return MaterialPage(
child: child,
key: Key(routeSettings.toString()),
name: routeSettings.name,
arguments: routeSettings.arguments,
);

现在更改地址栏中的 id 查询参数会重建我们的 RecipePage。

image.png

好吧,我们的功能现在是正确的,但是每次 id 更改时都对同一页面进行动画处理可能会让我们的用户感到不快和困惑。让我们通过扩展 TransitionDelegate 类来解决这个问题。我们将复制 DefaultTransitionDelegate 并稍微修改它。公平警告,这将是本指南中迄今为止最难掌握的部分,但我们会尽可能顺利地完成它。

过渡委托
首先在路由器目录中创建一个 transition_delegate.dart 文件。在文件中,使用常量构造函数定义 MyTransitionDelegate。

class MyTransitionDelegate extends TransitionDelegate {
const MyTransitionDelegate() : super();
}

接下来,我们必须重写 resolve 方法。下一个代码块直接从 Flutter 的 DefaultTransitionDelegate 复制而来。也可以随意复制和粘贴它。为了简洁起见,我们不会遍历每一行。现在,知道执行以下步骤就足够了。

1.查看所有退出的路线并确定哪些路线(如果有的话)应该动画化,同时考虑无页路线
2.查看所有进入的路线并确定哪些路线(如果有的话)应该在其中设置动画

@override
Iterable<RouteTransitionRecord> resolve({
@required List<RouteTransitionRecord> newPageRouteHistory,
@required Map<RouteTransitionRecord, RouteTransitionRecord> locationToExitingPageRoute,
@required Map<RouteTransitionRecord, List<RouteTransitionRecord>> pageRouteToPagelessRoutes,
}) {
final List<RouteTransitionRecord> results = <RouteTransitionRecord>[];
// This method will handle the exiting route and its corresponding pageless
// route at this location. It will also recursively check if there is any
// other exiting routes above it and handle them accordingly.
void handleExitingRoute(RouteTransitionRecord location, bool isLast) {
final RouteTransitionRecord exitingPageRoute = locationToExitingPageRoute[location];
if (exitingPageRoute == null) return;
if (exitingPageRoute.isWaitingForExitingDecision) {
final bool hasPagelessRoute = pageRouteToPagelessRoutes.containsKey(exitingPageRoute);
final bool isLastExitingPageRoute = isLast && !locationToExitingPageRoute.containsKey(exitingPageRoute);
if (isLastExitingPageRoute && !hasPagelessRoute) {
exitingPageRoute.markForPop(
exitingPageRoute.route.currentResult
);
} else {
exitingPageRoute.markForComplete(
exitingPageRoute.route.currentResult
);
}
if (hasPagelessRoute) {
final List<RouteTransitionRecord> pagelessRoutes = pageRouteToPagelessRoutes[exitingPageRoute];
for (final RouteTransitionRecord pagelessRoute in pagelessRoutes) {
// It is possible that a pageless route that belongs to an exiting
// page-based route does not require exiting decision. This can
// happen if the page list is updated right after a Navigator.pop.
if (pagelessRoute.isWaitingForExitingDecision) {
if (isLastExitingPageRoute && pagelessRoute == pagelessRoutes.last) {
pagelessRoute.markForPop(
pagelessRoute.route.currentResult
);
} else {
pagelessRoute.markForComplete(
pagelessRoute.route.currentResult
);
}
}
}
}
}
results.add(exitingPageRoute);
// It is possible there is another exiting route above this exitingPageRoute.
handleExitingRoute(exitingPageRoute, isLast);
}
// Handles exiting route in the beginning of list.
handleExitingRoute(null, newPageRouteHistory.isEmpty);
for (final RouteTransitionRecord pageRoute in newPageRouteHistory) {
final bool isLastIteration = newPageRouteHistory.last == pageRoute;
if (pageRoute.isWaitingForEnteringDecision) {
if (!locationToExitingPageRoute.containsKey(pageRoute) && isLastIteration) {
pageRoute.markForPush();
} else {
pageRoute.markForAdd();
}
}
results.add(pageRoute);
handleExitingRoute(pageRoute, isLastIteration);
}
return results;
}

我们的目标是如果最上面的退出路由是“/recipe”并且最上面的进入路由也是“/recipe”,则防止进入/退出动画。为此,我们将比较两者的 RouteSettings.name 并将 bool 变量设置为结果。如果没有退出页面,我们可以在不检查的情况下制作动画。在结果声明下方添加以下内容。

final List<RouteTransitionRecord> results = <RouteTransitionRecord>[];
// add this declaration
final bool showAnimation = locationToExitingPageRoute.length == 0 ||
newPageRouteHistory.last.route.settings.name !=
locationToExitingPageRoute.values.last.route.settings.name;

然后我们将简单地在 if 语句中添加一个 showAnimation 检查来确定过渡是否是动画的。我们对无页路由的转换决策过程不做任何事情。

// line 35
// add check for our showAnimation variable here
if (isLastExitingPageRoute && !hasPagelessRoute && showAnimation) {
exitingPageRoute.markForPop(exitingPageRoute.route.currentResult);
} else {
exitingPageRoute
.markForComplete(exitingPageRoute.route.currentResult);
}
// line 72
// add check for our showAnimation variable here
if (!locationToExitingPageRoute.containsKey(pageRoute) &&
isLastIteration &&
showAnimation) {
pageRoute.markForPush();
} else {
pageRoute.markForAdd();
}

最后,我们在 RouterDelegate 的构建方法中将自定义的 TransitionDelegate 传递给 Navigator。

@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: List.of(_pages),
onPopPage: _onPopPage,
transitionDelegate: const MyTransitionDelegate(),
);
}

嗯,这并没有我想象的那么糟糕,是吧?不过,我们已经投入了大量工作。让我们来看看我们的最终产品。

image.png

奖励:我在地址栏中草率的输入已经证明我们对 404 错误的处理方法同样有效。耶!

结论
恭喜!您现在知道您完全有能力迁移或设计一个使用 Navigator 2.0 的 Flutter 应用程序,以便在移动、Web 甚至桌面上提供最佳导航体验。让我们回顾一下本指南的这一部分所涵盖的内容。

RouteInformationProvider 是将地址栏中的 URL 同步到应用导航的关键组件。
它使用从 RouterDelegate 返回的 currentConfiguration 来执行此操作。
为了代码的健壮性和更好地处理 URL 查询参数,将 Map 传递给 RouteSettings 的 arguments 变量是个好主意。
我们还学习了如何使用 TransitionDelegate 控制导航动画。
在本系列的第 3 部分也是最后一部分中,我们将看到在我们已经正确实施 Navigator 2.0 后在我们的应用程序中添加深度链接是多么容易。

谢谢阅读!请分享任何意见或问题。

翻译自文章:
https://medium.com/geekculture/a-simpler-guide-to-flutter-navigator-2-0-part-ii-cf294d9dbe
文章中的demo:https://github.com/theLee3/flutter_nav_demo

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容