八、页面导航
Flutter 应用可能有多个屏幕或页面。页面是一组功能。用户在不同的页面之间导航以使用不同的功能。像页面这样的概念在 Flutter 中被称为路由。路由不仅包括全屏页面,还包括模态对话框和弹出窗口。路线由Navigator
小工具管理。本章讨论与 Flutter 中页面导航相关的方法。
8.1 实现基本页面导航
问题
您需要基本的页面导航支持。
解决办法
使用Navigator.push()
导航到新路线,使用Navigator.pop()
导航到以前的路线。
讨论
路线由Navigator
小工具管理。导航器管理一堆路线。可以使用push()
方法将路由推入堆栈,使用pop()
方法将路由弹出堆栈。堆栈中的顶部元素是当前活动的路由。Navigator
是一个有状态小部件,其状态为NavigatorState
。要与 navigator 交互,可以使用 Navigator 的静态方法或获取一个NavigatorState
的实例。通过使用Navigator.of()
方法,您可以获得给定构建上下文的最近的封闭NavigatorState
实例。您可以显式创建Navigator
小部件,但是大多数时候您将使用由WidgetsApp
、MaterialApp
或CupertinoApp
小部件创建的Navigator
小部件。
使用抽象Route
类的实现来表示路线。例如,PageRoute
代表全屏模式路线,PopupRoute
代表在当前路线上叠加一个小工具的模式路线。PageRoute
和PopupRoute
类都是ModalRoute
类的子类。对于材质设计应用,创建全屏页面最简单的方法是使用MaterialPageRoute
类。MaterialPageRoute
使用WidgetBuilder
函数构建路线的内容。
在清单 8-1 中,Navigator.of(context)
获取要使用的NavigatorState
实例。推送给导航器的新路线是一个MaterialPageRoute
实例。新路线有一个按钮,使用NavigatorState.pop()
方法将当前路线弹出导航器。其实在使用Scaffold
widget 的时候,应用栏里自动添加了一个后退按钮,所以不需要使用显式的后退按钮。
class SimpleNavigationPage extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Simple Navigation'),
),
body: Center(
child: RaisedButton(
child: Text('Show page'),
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(builder: (context) {
return Scaffold(
appBar: AppBar(
title: Text('New Page'),
),
body: Center(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text('A new page'),
RaisedButton(
child: Text('Go back'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
),
),
);
}));
},
),
),
);
}
}
Listing 8-1Page navigation using Navigator
Navigator
类有像push()
和pop()
这样的静态方法,它们和NavigatorState
类中的相同方法做同样的事情,但是这些静态方法需要一个额外的BuildContext
参数。Navigator.push(context)
其实和Navigator.of(context).push()
一样。你可以选择使用任何一种方法。
8.2 使用命名路线
问题
您想要从不同的地方导航到相同的路线。
解决办法
使用带有Navigator.pushNamed()
方法的命名路线。
讨论
当使用Navigator.push()
方法将新路线推送到导航器时,使用构建器函数按需构建新路线。当路线可以从不同的地方导航时,这种方法不太适用,因为我们不想重复构建路线的代码。在这种情况下,使用命名路由是更好的选择。命名路由具有唯一的名称。Navigator.pushNamed()
方法使用名称来指定要推送到导航器的路线。
命名的路由需要先注册,然后才能导航到。注册命名路径最简单的方法是使用WidgetsApp
、MaterialApp
或CupertinoApp
构造函数的routes
参数。routes
参数是一个Map<String, WidgetBuilder>
对象,以关键字作为路线名称。路由名称通常采用类似路径的格式,以“/”开头。这类似于 web 应用组织页面的方式。例如,您可以使用像/log_in
、/orders
和/orders/1234
这样的路线名称。
在清单 8-2 中,按下“注册”按钮将指定的路线/sign_up
推送到导航器。
class LogInPage extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Log In'),
),
body: Center(
child: RaisedButton(
child: Text('Sign Up'),
onPressed: () {
Navigator.pushNamed(context, '/sign_up');
},
),
),
);
}
}
Listing 8-2Use named route
在清单 8-3 中,在routes
参数中注册了两条命名路径。
class PageNavigationApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'Page Navigation',
home: IndexPage(),
routes: {
'/sign_up': (context) => SignUpPage(),
'/log_in': (context) => LogInPage(),
},
);
}
}
Listing 8-3
Register named routes
8.3 在路线之间传递数据
问题
您希望在不同的路由之间传递数据。
解决办法
使用构造函数参数或RouteSettings
对象将数据传递给路由,使用Navigator.pop()
方法的result
参数从路由传递数据。
讨论
构建路径内容时,路径可能需要额外的数据。弹出时,路由也可能返回一些数据。例如,编辑用户详细信息的路由可能需要当前的详细信息作为输入,并返回更新的详细信息作为输出。根据导航路线的方式,有不同的方法在路线之间传递数据。
使用Navigator.push()
方法推送新路线时,最简单的方法是将数据作为WidgetBuilder
函数返回的 widget 的构造函数参数传递。使用Navigator.pop()
方法时,可以使用可选的result
参数将返回值传递给之前的路径。Navigator.push()
方法的返回值是一个Future<T>
对象。这个Future
对象将在弹出新推送的路线时被解析。解析的值是调用Navigator.pop()
方法时传递的返回值。如果使用后退按钮弹出路线,则解析值为null
。
在清单 8-4 中,UserDetails
类包含用户的名和姓。UserDetailsPage
显示用户的详细信息。当按下编辑按钮时,一条新路线被推送到导航器。新路由的内容是一个EditUserDetailsPage
小部件,将UserDetails
对象作为构造函数参数。新路由的返回值也是一个UserDetails
对象,用于更新UserDetailsPage
的状态。
class UserDetails {
UserDetails(this.firstName, this.lastName);
final String firstName;
final String lastName;
}
class UserDetailsPage extends StatefulWidget {
_UserDetailsPageState createState() => _UserDetailsPageState();
}
class _UserDetailsPageState extends State<UserDetailsPage> {
UserDetails _userDetails = UserDetails('John', 'Doe');
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('User Details'),
),
body: Column(
children: <Widget>[
Text('First name: ${_userDetails.firstName}'),
Text('Last name: ${_userDetails.lastName}'),
RaisedButton.icon(
label: Text('Edit (route builder)'),
icon: Icon(Icons.edit),
onPressed: () async {
UserDetails result = await Navigator.push(
context,
MaterialPageRoute<UserDetails>(
builder: (BuildContext context) {
return EditUserDetailsPage(_userDetails);
},
),
);
if (result != null) {
setState(() {
_userDetails = result;
});
}
},
),
],
),
);
}
}
Listing 8-4
User details page
在清单 8-5 中,EditUserDetailsPage
使用两个TextFormField
小部件来编辑用户详细信息。当按下保存按钮时,使用Navigator.pop()
方法返回更新后的UserDetails
对象。
class EditUserDetailsPage extends StatefulWidget {
EditUserDetailsPage(this.userDetails);
final UserDetails userDetails;
_EditUserDetailsPageState createState() =>
_EditUserDetailsPageState(userDetails);
}
class _EditUserDetailsPageState extends State<EditUserDetailsPage> {
_EditUserDetailsPageState(this._userDetails);
UserDetails _userDetails;
final GlobalKey<FormFieldState<String>> _firstNameKey = GlobalKey();
final GlobalKey<FormFieldState<String>> _lastNameKey = GlobalKey();
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Edit User Details'),
),
body: Column(
children: <Widget>[
TextFormField(
key: _firstNameKey,
decoration: InputDecoration(
labelText: 'First name',
),
initialValue: _userDetails.firstName,
),
TextFormField(
key: _lastNameKey,
decoration: InputDecoration(
labelText: 'Last name',
),
initialValue: _userDetails.lastName,
),
RaisedButton(
child: Text('Save'),
onPressed: () {
Navigator.pop(
context,
UserDetails(_firstNameKey.currentState?.value,
_lastNameKey.currentState?.value));
},
)
],
),
);
}
}
Listing 8-5Edit user details page
如果使用命名路由,可以使用Navigator.pushNamed()
方法的arguments
参数将数据传递给路由。在清单 8-6 中,使用pushNamed()
方法导航到当前UserDetails
对象的/edit_user
路线。
UserDetails result = await Navigator.pushNamed(
context,
'/edit_user',
arguments: _userDetails,
);
Listing 8-6Pass data to named route
被命名的路线/edit_user
被登记在MaterialApp
中。不能使用route
参数,因为您不能访问在构建器函数中传递给路线的数据。应使用WidgetsApp
、MaterialApp
或CupertinoApp
的onGenerateRoute
参数。onGenerateRoute
参数的类型为RouteFactory
,是函数类型Route (RouteSettings settings)
的 typedef。RouteSettings
类包含创建Route
对象时可能需要的数据。表 8-1 显示了RouteSettings
类的属性。
表 8-1
路由设置的属性
|名字
|
类型
|
描述
|
| — | — | — |
| name
| String
| 路线的名称。 |
| arguments
| Object
| 传递给路由的数据。 |
| isInitialRoute
| bool
| 此路线是否是推送到导航器的第一条路线。 |
当实现onGenerateRoute
函数时,需要根据提供的RouteSettings
对象返回路线。在清单 8-7 中,首先检查name
属性,然后返回一个内容为EditUserDetailsPage
的MeterialPageRoute
。RouteSettings
的arguments
属性在EditUserDetailsPage
构造函数中使用。arguments
属性的值是清单 8-6 中传递的UserDetails
对象。
MaterialApp(
onGenerateRoute: (RouteSettings settings) {
if (settings.name == '/edit_user') {
return MaterialPageRoute<UserDetails>(
settings: settings,
builder: (context) {
return EditUserDetailsPage(settings.arguments);
},
);
}
},
);
Listing 8-7Use onGenerateRoute
8.4 实现动态路径匹配
问题
您希望使用复杂的逻辑来匹配路由名称。
解决办法
使用onGenerateRoute
参数。
讨论
当使用WidgetsApp
的routes
参数注册命名路线时,只有整个路线名称可用于匹配Route
对象。如果想用复杂的逻辑将Route
对象与路线名称匹配,可以使用onGenerateRoute
参数和RouteSettings
对象。例如,您可以将所有以/order
开头的路线名称匹配到一个Route
对象。
在清单 8-8 中,所有以/order
开头的路线名称将使用OrderPage
导航到一条路线。
MaterialApp(
onGenerateRoute: (RouteSettings settings) {
if (settings.name.startsWith('/order')) {
return MaterialPageRoute(
settings: settings,
builder: (context) {
return OrderPage();
},
);
}
},
);
Listing 8-8Route matching
8.5 处理未知路线
问题
您希望处理导航到未知路线的情况。
解决办法
使用Navigator
、WidgetsApp
、MaterialApp
、CupertinoApp
的onUnknownRoute
参数。
讨论
可能会要求导航员导航到未知的路线。这可能是由于应用中的编程错误或外部路线导航请求造成的。如果onGenerateRoute
函数为给定的RouteSettings
对象返回null
,则onUnknownRoute
函数被调用以提供一条回退路线。这个onUnknownRoute
函数通常用于错误处理,就像 web 应用中的 404 页面一样。onUnknownRoute
的类型也是RouteFactory
。
在清单 8-9 中,onUnknownRoute
函数返回显示NotFoundPage
小部件的路线。
MaterialApp(
onUnknownRoute: (RouteSettings settings) {
return MaterialPageRoute(
settings: settings,
builder: (BuildContext context) {
return NotFoundPage(settings.name);
},
);
},
);
Listing 8-9Use onUnknownRoute
8.6 显示材质设计对话框
问题
您希望显示材质设计对话框。
解决办法
使用showDialog()
功能和Dialog
、SimpleDialog
和AlertDialog
控件。
讨论
要使用材质设计对话框,你需要创建对话框部件并显示它们。Dialog
类及其子类SimpleDialog
和AlertDialog
可以用来创建对话框。
SimpleDialog
小工具为用户提供了几个选项。选项使用SimpleDialogOption
类表示。一个SimpleDialogOption
小部件可以有一个子小部件和一个onPressed
回调。当创建SimpleDialog
时,你可以提供一个孩子列表和一个可选的标题。AlertDialog
widget 向用户呈现内容和动作列表。AlertDialog
用于确认用户或要求确认。
要显示对话框,应该使用showDialog()
功能。调用此函数会将对话路由推送到导航器。使用Navigator.pop()
方法关闭对话框。showDialog()
函数使用WidgetBuilder
函数构建对话内容。showDialog()
函数的返回值是一个Future<T>
对象,它实际上是Navigator.push()
方法的返回值。
在清单 8-10 中,按下按钮会显示一个带有两个选项的简单对话框。
RaisedButton(
child: Text('Show SimpleDialog'),
onPressed: () async {
String result = await showDialog<String>(
context: context,
builder: (BuildContext context) {
return SimpleDialog(
title: Text('Choose Color'),
children: <Widget>[
SimpleDialogOption(
child: Text('Red'),
onPressed: () {
Navigator.pop(context, 'Red');
},
),
SimpleDialogOption(
child: Text('Green'),
onPressed: () {
Navigator.pop(context, 'Green');
},
),
],
);
});
print(result);
},
);
Listing 8-10Show simple dialogs
图 8-1 显示了清单 8-10 中的代码截图。
图 8-1
材质设计简单对话框
在清单 8-11 中,按下按钮会显示一个带有两个动作的警告对话框。
RaisedButton(
child: Text('Show AlertDialog'),
onPressed: () async {
bool result = await showDialog<bool>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Delete'),
content: Text('Delete this item?'),
actions: <Widget>[
FlatButton(
child: Text('Yes'),
onPressed: () {
Navigator.pop(context, true);
},
),
FlatButton(
child: Text('No'),
onPressed: () {
Navigator.pop(context, false);
},
),
],
);
},
);
print(result);
},
);
Listing 8-11Show alert dialog
图 8-2 显示了清单 8-11 中的代码截图。
图 8-2
材质设计警告对话框
8.7 显示 iOS 对话框
问题
您想要显示 iOS 对话框。
解决办法
使用showCupertinoDialog()
功能和CupertinoAlertDialog
和CupertinoPopupSurface
控件。
讨论
对于 iOS 应用,你可以使用showCupertinoDialog()
功能和CupertinoAlertDialog
和CupertinoPopupSurface
等小工具来显示对话框。showCupertinoDialog()
功能与材质设计的showDialog()
功能类似。该函数也使用Navigator.push()
方法将对话路径推送到导航器。CupertinoAlertDialog
是一个内置的对话框实现,用于确认用户或要求确认。一个CupertinoAlertDialog
可能有标题、内容和动作列表。使用CupertinoDialogAction
小部件表示动作。表 8-2 显示了CupertinoDialogAction
构造器的参数。
表 8-2
CupertinoDialogAction 参数
|名字
|
类型
|
描述
|
| — | — | — |
| child
| Widget
| 行动的内容。 |
| onPressed
| VoidCallback
| 操作按了回拨。 |
| isDefaultAction
| bool
| 此操作是否为默认操作。 |
| isdstructural action | bool
| 这个动作是否具有破坏性。破坏性的行为有不同的风格。 |
| textStyle
| TextStyle
| 应用于操作的文本样式。 |
在清单 8-12 中,按下按钮会显示一个 iOS 风格的警告对话框。
CupertinoButton(
child: Text('Show Alert Dialog'),
onPressed: () async {
bool result = await showCupertinoDialog<bool>(
context: context,
builder: (BuildContext context) {
return CupertinoAlertDialog(
title: Text('Delete'),
content: Text('Delete this item?'),
actions: <Widget>[
CupertinoDialogAction(
child: Text('Delete'),
onPressed: () {
Navigator.pop(context, true);
},
isDestructiveAction: true,
),
CupertinoDialogAction(
child: Text('Cancel'),
onPressed: () {
Navigator.pop(context, false);
},
),
],
);
},
);
print(result);
},
);
Listing 8-12Show iOS alert dialog
图 8-3 显示了清单 8-12 中的代码截图。
图 8-3
iOS 警报对话框
如果你想创建一个自定义对话框,你可以使用CupertinoPopupSurface
小部件来创建圆角矩形表面。
8.8 显示 iOS 行动表
问题
您想要在 iOS 应用中呈现一组操作供用户选择。
解决办法
使用showCupertinoModalPopup()
功能和CupertinoActionSheet
控件。
讨论
如果想在 iOS 应用中呈现一组动作供用户选择,可以使用showCupertinoModalPopup()
函数显示CupertinoActionSheet
widgets。一个CupertinoActionSheet
可以有标题、消息、取消按钮和动作列表。动作被表示为CupertinoActionSheetAction
小部件。CupertinoActionSheetAction
构造器有参数child
、onPressed
、isDefaultAction
、isDestructiveAction
,与表 8-2 中的CupertinoDialogAction
构造器含义相同。
在清单 8-13 中,按下按钮会显示一个带有三个动作和一个取消按钮的动作表。
CupertinoButton(
child: Text('Show Action Sheet'),
onPressed: () async {
String result = await showCupertinoModalPopup<String>(
context: context,
builder: (BuildContext context) {
return CupertinoActionSheet(
title: Text('What to do'),
message: Text('Please select an action'),
actions: <Widget>[
CupertinoActionSheetAction(
child: Text('Duplicate'),
isDefaultAction: true,
onPressed: () {
Navigator.pop(context, 'duplicate');
},
),
CupertinoActionSheetAction(
child: Text('Move'),
onPressed: () {
Navigator.pop(context, 'move');
},
),
CupertinoActionSheetAction(
isDestructiveAction: true,
child: Text('Delete'),
onPressed: () {
Navigator.pop(context, 'delete');
},
),
],
cancelButton: CupertinoActionSheetAction(
child: Text('Cancel'),
onPressed: () {
Navigator.pop(context);
},
),
);
},
);
print(result);
},
);
Listing 8-13Show iOS action sheet
图 8-4 显示了清单 8-13 中的代码截图。
图 8-4
iOS 行动表
8.9 显示材质设计菜单
问题
你想在材质设计应用中显示菜单。
解决办法
使用showMenu()
函数和PopupMenuEntry
类的实现。
讨论
要使用showMenu()
函数,你需要有一个PopupMenuEntry
对象的列表。有不同类型的PopupMenuEntry
实现:
-
PopupMenuItem
–单值菜单项 -
CheckedPopupMenuItem
–带勾号的菜单项 -
PopupMenuDivider
–菜单项之间的水平分隔线
PopupMenuItem
是具有其值的类型的类属。表 8-3 显示了PopupMenuItem
构造器的参数。CheckedPopupMenuItem
是PopupMenuItem
的子类。CheckedPopupMenuItem
有checked
属性来指定是否显示复选标记。
表 8-3
PopupMenuItem 构造函数的参数
|名字
|
类型
|
描述
|
| — | — | — |
| child
| Widget
| 菜单项的内容。 |
| value
| T
| 菜单项的值。 |
| enabled
| bool
| 是否可以选择此菜单项。 |
| height
| double
| 菜单项的高度。默认为48
。 |
showMenu()
函数返回一个Future<T>
对象,该对象解析为所选菜单项的值。该功能也使用Navigator.push()
方法来显示菜单。表 8-4 显示了showMenu()
功能的主要参数。当指定initialValue
时,具有匹配值的第一个项目被高亮显示。
表 8-4
showMenu()的参数
|名字
|
类型
|
描述
|
| — | — | — |
| items
| List<PopupMenuEntry<T>>
| 菜单项列表。 |
| initialValue
| T
| 突出显示菜单项的初始值。 |
| position
| RelativeRect
| 显示菜单的位置。 |
清单 8-14 中的菜单包含一个PopupMenuItem
,一个PopupMenuDivider
,和一个CheckedPopupMenuItem
。
RaisedButton(
child: Text('Show Menu'),
onPressed: () async {
String result = await showMenu<String>(
context: context,
position: RelativeRect.fromLTRB(0, 0, 0, 0),
items: [
PopupMenuItem(
value: 'red',
child: Text('Red'),
),
PopupMenuDivider(),
CheckedPopupMenuItem(
value: 'green',
checked: true,
child: Text('Green'),
)
],
initialValue: 'green',
);
print(result);
},
);
Listing 8-14Show menu
使用showMenu()
函数的主要困难是为position
参数提供合适的值。如果菜单是按下按钮触发的,使用PopupMenuButton
是更好的选择,因为菜单位置是根据按钮的位置自动计算的。表 8-5 显示了PopupMenuButton
构造器的主要参数。PopupMenuItemBuilder
函数将一个BuildContext
对象作为参数,并返回一个List<PopupMenuEntry<T>>
对象。
表 8-5
弹出菜单按钮的参数
|名字
|
类型
|
描述
|
| — | — | — |
| itemBuilder
| PopupMenuItemBuilder<T>
| 用于创建菜单项的生成器函数。 |
| initialValue
| T
| 初始值。 |
| onSelected
| PopupMenuItemSelected<T>
| 选择菜单项时回调。 |
| onCanceled
| PopupMenuCanceled
| 当菜单没有选择就被关闭时回调。 |
| tooltip
| String
| 按钮的工具提示。 |
| child
| Widget
| 按钮的内容。 |
| icon
| Icon
| 按钮的图标。 |
清单 8-15 展示了如何使用PopupMenuButton
来实现与清单 8-14 中相同的菜单。
PopupMenuButton(
itemBuilder: (BuildContext context) {
return <PopupMenuEntry<String>>[
PopupMenuItem(
value: 'red',
child: Text('Red'),
),
PopupMenuDivider(),
CheckedPopupMenuItem(
value: 'green',
checked: true,
child: Text('Green'),
)
];
},
initialValue: 'green',
child: Text('Select color'),
onSelected: (String value) {
print(value);
},
onCanceled: () {
print('no selections');
},
);
Listing 8-15Use PopupMenuButton
图 8-5 显示了在清单 8-14 和 8-15 中创建的菜单截图。
图 8-5
材质设计菜单
8.10 使用嵌套导航器管理复杂的页面流
问题
你想要复杂的页面流。
解决办法
使用嵌套的Navigator
实例。
讨论
一个Navigator
实例管理它自己的路由栈。对于简单的 app,一个Navigator
实例一般就够了,可以简单使用WidgetsApp
、MaterialApp
或者CupertinoApp
创建的Navigator
实例。如果你的应用有复杂的页面流,你可能需要使用嵌套导航器。由于Navigator
本身也是一个小部件,Navigator
实例可以像普通小部件一样创建。由WidgetsApp
、MaterialApp
或CupertinoApp
创建的Navigator
实例成为根导航器。所有导航器都是按层次结构组织的。要获得根导航器,可以在调用Navigator.of()
方法时将rootNavigator
参数设置为true
。表 8-6 显示了Navigator
构造器的参数。
表 8-6
导航器参数
|名字
|
类型
|
描述
|
| — | — | — |
| onGenerateRoute
| RouteFactory
| 为给定的RouteSettings
对象生成路线。 |
| onUnknownRoute
| RouteFactory
| 处理未知路线。 |
| initialRoute
| String
| 第一条路线的名称。 |
| observers
| List<NavigatorObserver>
| 导航器中状态变化的观察者。 |
让我们用一个具体的例子来解释如何使用嵌套导航器。假设您正在构建一个社交新闻阅读应用,在新用户注册后,您希望向用户显示一个可选的登录页面。这个入门页面有几个步骤需要用户完成。用户可以来回移动,只完成感兴趣的步骤。用户也可以跳过这个页面,返回到应用的主页。入门页面有自己的导航器来处理步骤导航。
在清单 8-16 中,导航器有两条命名的路线。初始路线设置为on_boarding/topic
,所以先显示UserOnBoardingTopicPage
。
class UserOnBoardingPage extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Get Started'),
),
body: Navigator(
initialRoute: 'on_boarding/topic',
onGenerateRoute: (RouteSettings settings) {
WidgetBuilder builder;
switch (settings.name) {
case 'on_boarding/topic':
builder = (BuildContext context) {
return UserOnBoardingTopicPage();
};
break;
case 'on_boarding/follower':
builder = (BuildContext context) {
return UserOnBoardingFollowPage();
};
break;
}
return MaterialPageRoute(
builder: builder,
settings: settings,
);
},
),
);
}
}
Listing 8-16User on-boarding page
在清单 8-17 中,按下“下一步”按钮导航到路线名称为on_boarding/follower
的下一步。按下“完成”按钮,使用根导航器弹出登机页面。
class UserOnBoardingTopicPage extends StatelessWidget {
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Text('Select interested topics'),
RaisedButton.icon(
icon: Icon(Icons.arrow_forward),
label: Text('Next'),
onPressed: () {
Navigator.pushNamed(context, 'on_boarding/follower');
},
),
RaisedButton.icon(
icon: Icon(Icons.check),
label: Text('Done'),
onPressed: () {
Navigator.of(context, rootNavigator: true).pop();
},
)
],
);
}
}
Listing 8-17Step to select topics
图 8-6 显示了清单 8-17 中的代码截图。
图 8-6
选择主题的步骤
CupertinoTabView
有自己的导航器实例。创建CupertinoTabView
时,可以提供routes
、onGenerateRoute
、onUnknownRoute
、navigatorObservers
参数。这些参数用于配置Navigator
实例。当使用CupertinoTabScaffold
创建选项卡布局时,每个选项卡视图都有自己的导航状态和历史。
使用嵌套导航器时,确保使用正确的导航器实例很重要。如果要显示和关闭全屏页面或模态对话框,应该使用Navigator.of(context, rootNavigator: true)
获得的根导航器。调用Navigator.of(context)
只能获得最近的封闭Navigator
实例。没有办法获得层次结构中的中间Navigator
实例。您需要在窗口小部件树的正确位置使用BuildContext
对象。像showDialog()
和showMenu()
这样的函数总是在内部使用Navigator.of(context)
。您只能使用传入的BuildContext
对象来控制这些函数使用哪个Navigator
实例。
8.11 观察导航器状态变化
问题
您希望在导航状态改变时得到通知。
解决办法
使用NavigatorObserver
。
讨论
有时,您可能希望在导航器状态改变时得到通知。例如,您希望分析使用应用的用户的页面流量,以改善用户体验。当创建Navigator
实例时,您可以提供一个NavigatorObserver
对象的列表,作为导航器状态变化的观察者。表 8-7 显示了NavigatorObserver
接口的方法。
表 8-7
导航观测方法
|名字
|
描述
|
| — | — |
| didPop(Route route, Route previousRoute)
| 弹出route
并且previousRoute
是新激活的路由。 |
| didPush(Route route, Route previousRoute)
| 按下route
,而previousRoute
是先前激活的路线。 |
| didRemove(Route route, Route previousRoute)
| route
被删除,previousRoute
是被删除路线的下一条路线。 |
| didReplace(Route newRoute, Route oldRoute)
| 将oldRoute
替换为newRoute
。 |
| didStartUserGesture(Route route, Route previousRoute)
| 用户开始使用手势移动route
。路线正下方的路线是previousRoute
。 |
| didStopUserGesture()
| 用户使用手势停止移动路线。 |
在清单 8-18 中,LoggingNavigatorObserver
类记录路由推送和弹出时的消息。
class LoggingNavigatorObserver extends NavigatorObserver {
void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
print('push: ${_routeName(previousRoute)} -> ${_routeName(route)}');
}
void didPop(Route<dynamic> route, Route<dynamic> previousRoute) {
print(' pop: ${_routeName(route)} -> ${_routeName(previousRoute)}');
}
String _routeName(Route<dynamic> route) {
return route != null
? (route.settings?.name ?? route.runtimeType.toString())
: 'null';
}
}
Listing 8-18Logging navigator observer
当你想用一个全局处理程序来处理导航器中的所有状态变化时,接口是很有用的。如果您只对与特定路由相关的状态变化感兴趣,那么使用RouteObserver
类是更好的选择。RouteObserver
类也是NavigatorObserver
接口的一个实现。
为了获得与一个Route
对象相关的状态变化的通知,您的类需要实现RouteAware
接口。表 8-8 显示了RouteAware
接口的方法。
表 8-8
路由软件的方法
|名字
|
描述
|
| — | — |
| didPop()
| 弹出当前路径时回调。 |
| didPopNext()
| 当顶层路由弹出后当前路由变为活动时调用。 |
| didPush()
| 当当前路线被推送时调用。 |
| didPushNext()
| 推送新路由后,当前路由不再活动时调用。 |
要真正得到一个Route
对象的通知,您需要使用RouteObserver
的subscribe()
方法将一个RouteAware
对象订阅给一个Route
对象。当不再需要订阅时,您应该使用unsubscribe()
取消订阅RouteAware
对象。
在清单 8-19 中,_ObservedPageState
类实现了RouteAware
接口并覆盖了didPush()
和didPop()
方法来打印出一些消息。ModalRoute.of(context)
从构建上下文中获取最近的封闭ModalRoute
对象,这是ObservedPage
所在的路径。通过使用ModalRoute.of(context)
,不需要显式传递Route
对象。当前_ObservedPageState
对象使用传入的RouteObserver
对象的subscribe()
方法订阅当前路由中的状态变化。当_ObservedPageState
对象被释放时,订阅被删除。
class ObservedPage extends StatefulWidget {
ObservedPage(this.routeObserver);
final RouteObserver<PageRoute<dynamic>> routeObserver;
_ObservedPageState createState() => _ObservedPageState(routeObserver);
}
class _ObservedPageState extends State<ObservedPage> with RouteAware {
_ObservedPageState(this._routeObserver);
final RouteObserver<PageRoute<dynamic>> _routeObserver;
void didChangeDependencies() {
super.didChangeDependencies();
_routeObserver.subscribe(this, ModalRoute.of(context));
}
void dispose() {
_routeObserver.unsubscribe(this);
super.dispose();
}
void didPush() {
print('pushed');
}
void didPop() {
print('popped');
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Observed (Stateful)'),
),
);
}
}
Listing 8-19Use RouteObserver
8.12 阻止路线弹出
问题
您希望阻止路线弹出导航器。
解决办法
将WillPopCallback
与ModalRoute
对象一起使用。
讨论
当一条路线被推送到导航器时,可以使用Scaffold
中的后退按钮或 Android 中的系统后退按钮弹出该路线。有时,您可能想要阻止路线被弹出。例如,如果页面中有未保存的更改,您可能希望首先显示一个警告对话框来要求确认。当使用Navigator.maybePop()
方法而不是Navigator.pop()
方法时,您有机会决定弹出路线的请求是否应该继续。
ModalRoute
类有addScopedWillPopCallback()
方法来添加WillPopCallback
来决定是否弹出路线。WillPopCallback
是函数类型Future<bool> ()
的 typedef。如果返回的Future<bool>
对象解析为true
,则可以弹出路线。否则,无法弹出路线。您可以向一个 ModalRoute 对象添加多个WillPopCallback
函数。如果WillPopCallback
函数中的任何一个否决了该请求,则不会弹出该路线。
在清单 8-20 中,一个WillPopCallback
功能被添加到当前路线中。WillPopCallback
函数的返回值是showDialog()
返回的Future<bool>
对象。
class VetoPopPage extends StatelessWidget {
Widget build(BuildContext context) {
ModalRoute.of(context).addScopedWillPopCallback(() {
return showDialog<bool>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: Text('Exit?'),
actions: <Widget>[
FlatButton(
child: Text('Yes'),
onPressed: () {
Navigator.pop(context, true);
},
),
FlatButton(
child: Text('No'),
onPressed: () {
Navigator.pop(context, false);
},
),
],
);
},
);
});
return Scaffold(
appBar: AppBar(
title: Text('Veto Pop'),
),
body: Container(),
);
}
}
Listing 8-20Veto route popping request
8.13 摘要
在 Flutter 应用中有多个页面是很常见的。本章讨论在 Flutter 中实现页面导航的基本概念。本章还包括对话框、菜单和动作表。在下一章,我们将讨论 Flutter 中的后端服务交互。
九、服务交互
许多重要的移动应用需要与后端服务进行交互。本章涵盖了与 Flutter 中的服务交互相关的基本概念。
9.1 使用期货
问题
你想处理Future
物体。
解决办法
使用then()
和catchError()
方法处理Future
对象的结果。
讨论
当使用来自 Flutter 和 Dart 库的代码时,您可能会遇到返回Future
对象的函数。来自dart:async
库的Future<T>
类是延迟计算的代表。一个Future
对象代表一个将来可能出现的潜在值或错误。当给定一个Future
对象时,可以注册回调来处理可用的值或错误。Future
类是 Dart 中异步编程的基本构建块之一。
给定一个Future
对象,关于其结果有三种不同的情况:
-
计算永远不会完成。不会调用任何回调。
-
计算以一个值结束。使用值调用值回调。
-
计算完成时出现错误。错误回调与错误一起被调用。
要注册对Future
对象的回调,可以使用then()
方法注册一个值回调和一个可选的错误回调,或者使用catchError()
方法只注册一个错误回调。建议使用then()
方法只注册一个值回调。这是因为如果一个错误回调是使用then()
方法的onError
参数注册的,这个错误回调不能处理在值回调中抛出的错误。大多数情况下,您希望错误回调处理所有可能的错误。如果一个Future
对象的错误没有被它的错误回调函数处理,这个错误将被全局处理程序处理。
在清单 9-1 中,Future
对象可能以值1
或一个Error
对象结束。值和错误回调都被注册来处理结果。
Future.delayed(
Duration(seconds: 1),
() {
if (Random().nextBool()) {
return 1;
} else {
throw Error();
}
},
).then((value) {
print(value);
}).catchError((error) {
print('error: $error');
});
Listing 9-1Use then() and catchError() methods to handle result
then()和 catchError()方法的返回值也是Future
对象。给定一个Future
对象 A,调用A.then(func)
的结果是另一个Future
对象 B,如果func
回调运行成功,Future
B 将以调用func
函数的返回值完成。否则,Future B 将会以调用func
函数时抛出的错误完成。调用B.catchError(errorHandler)
返回一个新的Future
对象 c。错误处理程序可以处理在Future
B 中抛出的错误,这些错误可能会在未来的 A 本身或其值处理程序中抛出。通过使用then()
和catchError()
方法,Future
对象形成了一个处理异步计算的链。
在清单 9-2 中,多个then()
方法被链接在一起按顺序处理结果。
Future.value(1)
.then((value) => value + 1)
.then((value) => value * 10)
.then((value) => value + 2)
.then((value) => print(value));
Listing 9-2
Chained then() methods
如果你想在未来完成时调用函数,你可以使用whenComplete()
方法。当这个 future 完成时,使用whenComplete()
添加的函数被调用,不管它完成时是有值还是有错误。whenComplete()
方法相当于其他编程语言中的finally
块。then().catchError().whenComplete()
的链条相当于“尝试-捕捉-最终”。
清单 9-3 展示了一个使用whenComplete()
方法的例子。
Future.value(1).then((value) {
print(value);
}).whenComplete(() {
print('complete');
});
Listing 9-3Using whenComplete()
Future
对象的计算可能需要很长时间才能完成。您可以使用timeout()
方法来设置计算的时间限制。当调用timeout()
方法时,需要提供一个Duration
对象作为时间限制,并提供一个可选的onTimeout
函数在超时发生时提供值。timeout()
方法的返回值是一个新的Future
对象。如果当前的Future
对象没有在时限前完成,调用onTimeout
函数的结果就是新的Future
对象的结果。如果没有提供onTimeout
函数,当当前未来超时时,新的Future
对象将以TimeoutException
结束。
在清单 9-4 中,Future
对象将用值1
在 5 秒内完成,但是时间限制被设置为 2 秒。将使用由onTimeout
函数返回的值10
。
Future.delayed(Duration(seconds: 5), () => 1)
.timeout(
Duration(seconds: 2),
onTimeout: () => 10,
)
.then((value) => print(value));
Listing 9-4Use timeout() method
9.2 使用异步和等待来处理期货
问题
你想要处理Future
对象,就像它们是同步的一样。
解决办法
使用async
和await
。
讨论
对象代表异步计算。使用Future
对象的通常方式是注册回调来处理结果。这种基于回调的风格可能会给习惯同步操作的开发人员造成障碍。使用async
和await
是 Dart 中的一个语法糖,可以像普通同步操作一样处理Future
对象。
给定一个Future
对象,await
可以等待其完成并返回其值。await
之后的代码可以直接使用返回值,就像它是同步调用的结果一样。使用await
时,其封闭功能必须标记为async
。这意味着该函数返回一个Future
对象。
在清单 9-5 中,getValue()
函数的返回值是一个Future
对象。在calculate()
函数中,await
用于获取getValue()
函数的返回值并赋给value
变量。由于使用了await
,所以calculate()
功能被标记为async
。
Future<int> getValue() {
return Future.value(1);
}
Future<int> calculate() async {
int value = await getValue();
return value * 10;
}
Listing 9-5Use async/await
当使用await
处理Future
对象时,可以使用 try-catch-finally 处理Future
对象中抛出的错误。这允许像普通同步操作一样使用Future
对象。清单 9-6 展示了一起使用 try-catch-finally 和await
/ async
的例子。
Future<int> getErrorValue() {
return Future.error('invalid value');
}
Future<int> calculateWithError() async {
try {
return await getErrorValue();
} catch (e) {
print(e);
return 1;
} finally {
print('done');
}
}
Listing 9-6Use try-catch-finally and await/async
9.3 创造未来
问题
你想要创建Future
对象。
解决办法
使用Future
构造函数Future()
、Future.delayed()
、Future.sync()
、Future.value()
和Future.error()
创建Future
对象。
讨论
如果需要创建Future
对象,可以使用它的构造函数Future()
、Future.delayed()
、Future.sync()
、Future.value()
和Future.error()
:
-
Future()
构造函数创建一个Future
对象,异步运行计算。 -
Future.delayed()
构造函数创建一个Future
对象,该对象在使用Duration
对象指定的延迟之后运行计算。 -
Future.sync()
构造函数创建一个Future
对象,它立即运行计算。 -
Future.value()
构造函数创建一个Future
对象,用给定的值完成。 -
Future.error()
构造函数创建一个Future
对象,该对象以给定的错误和可选的堆栈跟踪完成。
清单 9-7 展示了使用不同Future
构造函数的例子。
Future(() => 1).then(print);
Future.delayed(Duration(seconds: 3), () => 1).then(print);
Future.sync(() => 1).then(print);
Future.value(1).then(print);
Future.error(Error()).catchError(print);
Listing 9-7Create Future objects
9.4 使用流
问题
你想处理一连串的事件。
解决办法
使用Stream<T>
类及其子类。
讨论
使用Future
类,我们可以表示将来可能可用的单个值。然而,我们可能还需要处理一系列事件。dart:async
库中的Stream<T>
类表示异步事件的来源。为了帮助解决这个问题,Future
类有asStream()
方法来创建一个包含当前Future
对象结果的Stream
。
如果您有使用反应流( www.reactive-streams.org/
)的经验,您可能会发现 Dart 中的Stream
是一个类似的概念。流中可以有三种类型的事件:
-
数据事件表示流中的实际数据。这些事件也称为流中的元素。
-
Error 事件表示发生了错误。
-
Done 事件表示已到达流的末尾。不会发出更多事件。
要从流中接收事件,可以使用listen()
方法来设置监听器。listen()
方法的返回值是一个代表活动订阅的StreamSubscription
对象。根据流上允许的订阅数量,有两种类型的流:
-
单订阅流在流的整个生命周期中只允许一个侦听器。它仅在侦听器建立时开始发出事件,在侦听器取消订阅时停止发出事件。
-
广播流允许任意数量的听众。即使没有订阅的侦听器,事件也会在准备就绪时发出。
给定一个Stream
对象,属性isBroadcast
可以用来检查它是否是一个广播流。您可以使用asBroadcastStream()
方法从单一订阅流创建广播流。
流订阅
表 9-1 显示了listen()
方法的参数。您可以为不同的事件提供任意数量的处理程序,并忽略那些不感兴趣的事件。
表 9-1
listen()方法的参数
|名字
|
类型
|
描述
|
| — | — | — |
| onData
| void (T event)
| 数据事件的处理程序。 |
| onError
| Function
| 错误事件的处理程序。 |
| onDone
| void ()
| done 事件的处理程序。 |
| cancelOnError
| bool
| 发出第一个错误事件时是否取消订阅。 |
在清单 9-8 中,提供了三种类型事件的处理程序。
Stream.fromIterable([1, 2, 3]).listen(
(value) => print(value),
onError: (error) => print('error: $error'),
onDone: () => print('done'),
cancelOnError: true,
);
Listing 9-8Use listen() method
使用由listen()
方法返回的StreamSubscription
对象,您可以管理订阅。表 9-2 展示了StreamSubscription
类的方法。
表 9-2
流订阅的方法
|名字
|
描述
|
| — | — |
| cancel()
| 取消此订阅。 |
| pause([Future resumeSignal])
| 请求流暂停事件发出。如果提供了 resumeSignal,流将在未来完成时恢复。 |
| resume()
| 暂停后继续流。 |
| onData()
| 替换数据事件处理程序。 |
| onError()
| 替换错误事件处理程序。 |
| onDone()
| 替换 done 事件处理程序。 |
| asFuture([E futureValue])
| 返回处理流完成的未来值。 |
当您想要处理流的完成时,asFuture()
方法很有用。因为一个流可以正常完成,也可以出错,所以使用这个方法会覆盖现有的onDone
和onError
回调。在发生错误事件的情况下,订阅被取消,返回的Future
对象完成时出现错误。在完成事件的情况下,Future
对象以给定的futureValue
结束。
流转换
stream 的强大之处在于对流应用各种转换来获得另一个流或值。表 9-3 显示了返回另一个Stream
对象的Stream
类中的方法。
表 9-3
流转换
|名字
|
描述
|
| — | — |
| asyncExpand<E>(Stream<E> convert(T event))
| 将每个元素转换成一个流,并将这些流中的元素连接成新的流。 |
| asyncMap<E>(FutureOr<E> convert(T event))
| 将每个元素转换成一个新事件。 |
| distinct([bool equals(T previous, T next) ])
| 跳过重复的元素。 |
| expand<S>(Iterable<S> convert(T element))
| 将每个元素转换为一系列元素。 |
| handleError(Function onError, { bool test(dynamic error) })
| 处理流中的错误。 |
| map<S>(S convert(T event))
| 将每个元素转换成一个新事件。 |
| skip(int count)
| 跳过流中的元素。 |
| skipWhile(bool test(T element))
| 跳过与谓词匹配的元素。 |
| take(int count)
| 仅从流中获取前 count 个元素。 |
| takeWhile(bool test(T element))
| 获取与谓词匹配的元素。 |
| timeout(Duration timeLimit, { void onTimeout(EventSink<T> sink) })
| 当两个事件之间的时间超过时间限制时处理错误。 |
| transform<S>(StreamTransformer<T, S> streamTransformer)
| 转换流。 |
| where(bool test(T event))
| 过滤流中的元素。 |
清单 9-9 展示了使用流转换的例子。每个语句下面的代码显示了执行的结果。
Stream.fromIterable([1, 2, 3]).asyncExpand((int value) {
return Stream.fromIterable([value * 5, value * 10]);
}).listen(print);
// -> 5, 10, 10, 20, 15, 30
Stream.fromIterable([1, 2, 3]).expand((int value) {
return [value * 5, value * 10];
}).listen(print);
// -> 5, 10, 10, 20, 15, 30
Stream.fromIterable([1, 2, 3]).asyncMap((int value) {
return Future.delayed(Duration(seconds: 1), () => value * 10);
}).listen(print);
// -> 10, 20, 30
Stream.fromIterable([1, 2, 3]).map((value) => value * 10).listen(print);
// -> 10, 20, 30
Stream.fromIterable([1, 1, 2]).distinct().listen(print);
// -> 1, 2
Stream.fromIterable([1, 2, 3]).skip(1).listen(print);
// -> 2, 3
Stream.fromIterable([1, 2, 3])
.skipWhile((value) => value % 2 == 1)
.listen(print);
// -> 2, 3
Stream.fromIterable([1, 2, 3]).take(1).listen(print);
// -> 1
Stream.fromIterable([1, 2, 3])
.takeWhile((value) => value % 2 == 1)
.listen(print);
// -> 1
Stream.fromIterable([1, 2, 3]).where((value) => value % 2 == 1).listen(print);
// -> 1, 3
Listing 9-9Stream transformations
在Stream
类中有其他方法返回一个Future
对象;见表 9-4 。这些操作返回单个值,而不是流。
表 9-4
单一值的方法
|名字
|
描述
|
| — | — |
| any(bool test(T element))
| 检查流中是否有任何元素与谓词匹配。 |
| every(bool test(T element))
| 检查流中的所有元素是否都与谓词匹配。 |
| contains(Object needle)
| 检查流中是否包含给定的元素。 |
| drain<E>([E futureValue ])
| 丢弃流中的所有元素。 |
| elementAt(int index)
| 获取给定索引处的元素。 |
| firstWhere(bool test(T element), { T orElse() })
| 查找与谓词匹配的第一个元素。 |
| lastWhere(bool test(T element), { T orElse() })
| 查找与谓词匹配的最后一个元素。 |
| singleWhere(bool test(T element), { T orElse() })
| 查找与谓词匹配的单个元素。 |
| fold<S>(S initialValue, S combine(S previous, T element))
| 将流中的元素组合成一个值。 |
| forEach(void action(T element))
| 对流中的每个元素运行操作。 |
| join([String separator = "" ])
| 将元素组合成一个字符串。 |
| pipe(StreamConsumer<T> streamConsumer)
| 将事件通过管道传输到 StreamConsumer。 |
| reduce(T combine(T previous, T element))
| 将流中的元素组合成一个值。 |
| toList()
| 将元素收集到一个列表中。 |
| toSet()
| 将元素收集到一个集合中。 |
清单 9-10 显示了使用表 9-4 中方法的例子。每个语句下面的代码显示了执行的结果。
Stream.fromIterable([1, 2, 3]).forEach(print);
// -> 1, 2, 3
Stream.fromIterable([1, 2, 3]).contains(1).then(print);
// -> true
Stream.fromIterable([1, 2, 3]).any((value) => value % 2 == 0).then(print);
// -> true
Stream.fromIterable([1, 2, 3]).every((value) => value % 2 == 0).then(print);
// -> false
Stream.fromIterable([1, 2, 3]).fold(0, (v1, v2) => v1 + v2).then(print);
// -> 6
Stream.fromIterable([1, 2, 3]).reduce((v1, v2) => v1 * v2).then(print);
// -> 6
Stream.fromIterable([1, 2, 3])
.firstWhere((value) => value % 2 == 1)
.then(print);
// -> 1
Stream.fromIterable([1, 2, 3])
.lastWhere((value) => value % 2 == 1)
.then(print);
// -> 3
Stream.fromIterable([1, 2, 3])
.singleWhere((value) => value % 2 == 1)
.then(print);
// -> Unhandled exception: Bad state: Too many elements
Listing 9-10Methods return Future objects
9.5 创建流
问题
你想要创建Stream
对象。
解决办法
使用不同的Stream
构造函数。
讨论
有不同的Stream
构造函数来创建Stream
对象:
-
Stream.empty()
构造器创建一个空的广播流。 -
Stream.fromFuture()
构造函数从一个Future
对象创建一个单一订阅流。 -
Stream.fromFutures()
构造器从一列Future
对象中创建一个流。 -
Stream.fromInterable()
构造函数从一个Iterable
对象的元素中创建一个单一订阅流。 -
Stream.periodic()
构造器创建一个流,它以给定的时间间隔周期性地发出数据事件。
清单 9-11 展示了不同Stream
构造函数的例子。
Stream.fromIterable([1, 2, 3]).listen(print);
Stream.fromFuture(Future.value(1)).listen(print);
Stream.fromFutures([Future.value(1), Future.error('error'), Future.value(2)])
.listen(print);
Stream.periodic(Duration(seconds: 1), (int count) => count * 2)
.take(5)
.listen(print);
Listing 9-11Use Stream constructors
另一种创建流的方法是使用StreamController
类。一个StreamController
对象可以向它控制的流发送不同的事件。默认的StreamController()
构造器创建一个单一订阅流,而StreamController.broadcast()
构造器创建一个广播流。使用StreamController
,您可以以编程方式在流中生成元素。
在清单 9-12 中,不同的事件被发送到由StreamController
对象控制的流中。
StreamController<int> controller = StreamController();
controller.add(1);
controller.add(2);
controller.stream.listen(print, onError: print, onDone: () => print('done'));
controller.addError('error');
controller.add(3);
controller.close();
Listing 9-12Use StreamController
9.6 基于流和未来构建小部件
问题
您希望构建一个基于流或未来数据更新其内容的小部件。
解决办法
使用StreamBuilder<T>
或FutureBuilder<T>
小工具。
讨论
给定一个Steam
或Future
对象,您可能想要构建一个基于其中的数据更新其内容的小部件。您可以使用StreamBuilder<T>
小部件处理Stream
对象,使用FutureBuilder<T>
小部件处理Future
对象。表 9-5 显示了StreamBuilder<T>
构造器的参数。
表 9-5
StreamBuilder 的参数
|名字
|
类型
|
描述
|
| — | — | — |
| stream
| Stream<T>
| 建造者的河流。 |
| builder
| AsyncWidgetBuilder<T>
| 小部件的构建器函数。 |
| initialData
| T
| 构建小部件的初始数据。 |
AsyncWidgetBuilder
是函数类型Widget (BuildContext context, AsyncSnapshot<T> snapshot)
的 typedef。AsyncSnapshot
类表示与异步计算交互的快照。表 9-6 显示了AsyncSnapshot<T>
类的属性。
表 9-6
异步快照的属性
|名字
|
类型
|
描述
|
| — | — | — |
| connectionState
| ConnectionState
| 异步计算的连接状态。 |
| data
| T
| 异步计算接收的最新数据。 |
| error
| Object
| 异步计算收到的最新错误对象。 |
| hasData
| bool
| 数据属性是否不是null
。 |
| hasError
| bool
| 错误属性是否不是null
。 |
您可以使用connectionState
的值来确定连接状态。表 9-7 显示了ConnectionState
枚举的值。
表 9-7
ConnectionState 的值
|名字
|
描述
|
| — | — |
| none
| 未连接到异步计算。 |
| waiting
| 连接到异步计算并等待交互。 |
| active
| 连接到活动的异步计算。 |
| done
| 连接到终止的异步计算。 |
使用StreamBuilder
widget 构建 UI 时,典型的方式是根据连接状态返回不同的 widget。例如,如果连接状态正在等待,则可以返回进程指示符。
在清单 9-13 中,流有五个每秒生成的元素。如果连接状态为none
或waiting
,则返回一个CircularProgressIndicator
小工具。如果状态为active
或done
,则根据data
和error
属性的值返回一个Text
小工具。
class StreamBuilderPage extends StatelessWidget {
final Stream<int> _stream =
Stream.periodic(Duration(seconds: 1), (int value) => value * 10).take(5);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Stream Builder'),
),
body: Center(
child: StreamBuilder(
stream: _stream,
initialData: 0,
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
case ConnectionState.waiting:
return CircularProgressIndicator();
case ConnectionState.active:
case ConnectionState.done:
if (snapshot.hasData) {
return Text('${snapshot.data ?? "}');
} else if (snapshot.hasError) {
return Text(
'${snapshot.error}',
style: TextStyle(color: Colors.red),
);
}
}
return null;
},
),
),
);
}
}
Listing 9-13Use StreamBuilder
FutureBuilder
控件的用法与StreamBuilder
控件类似。当使用带有Future
对象的FutureBuilder
时,可以先使用asStream()
方法将Future
对象转换为Stream
对象,然后对转换后的Stream
对象使用StreamBuilder
。
在清单 9-14 中,我们使用了一种不同的方式来构建 UI。使用hasData
和hasError
属性来检查状态,而不是检查连接状态。
class FutureBuilderPage extends StatelessWidget {
final Future<int> _future = Future.delayed(Duration(seconds: 1), () {
if (Random().nextBool()) {
return 1;
} else {
throw 'invalid value';
}
});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Future Builder'),
),
body: Center(
child: FutureBuilder(
future: _future,
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
if (snapshot.hasData) {
return Text('${snapshot.data}');
} else if (snapshot.hasError) {
return Text(
'${snapshot.error}',
style: TextStyle(color: Colors.red),
);
} else {
return CircularProgressIndicator();
}
},
),
),
);
}
}
Listing 9-14Use FutureBuilder
9.7 处理简单的 JSON 数据
问题
您希望有一种简单的方法来处理 JSON 数据。
解决办法
使用dart:convert
库中的jsonEncode()
和jsonDecode()
函数。
讨论
JSON 是一种流行的 web 服务数据格式。为了与后端服务交互,您可能需要在两种情况下处理 JSON 数据:
-
JSON 数据序列化将 Dart 中的对象转换为 JSON 字符串。
-
JSON 数据反序列化将 JSON 字符串转换为 Dart 中的对象。
对于这两种场景,如果您只是偶尔需要处理简单的 JSON 数据,那么使用dart:convert
库中的jsonEncode()
和jsonDecode()
函数是一个不错的选择。jsonEncode()
函数将镖对象转换成字符串,而jsonDecode()
函数将字符串转换成镖对象。在清单 9-15 中,数据对象首先被序列化为 JSON 字符串,然后 JSON 字符串再次被反序列化为 Dart 对象。
var data = {
'name': 'Test',
'count': 100,
'valid': true,
'list': [
1,
2,
{
'nested': 'a',
'value': 123,
},
],
};
String str = jsonEncode(data);
print(str);
Object obj = jsonDecode(str);
print(obj);
Listing 9-15Handle JSON data
dart:convert
库中的 JSON 编码器只支持有限数量的数据类型,包括数字、字符串、布尔、null
、列表和带字符串键的映射。要对其他类型的对象进行编码,您需要使用toEncodable
参数来提供一个函数,该函数首先将对象转换为可编码的值。默认的toEncodable
函数调用对象上的toJson()
方法。向需要序列化为 JSON 字符串的自定义类添加toJson()
方法是一种常见的做法。
在清单 9-16 中,ToEncode
类的toJson()
方法返回一个列表,该列表将作为 JSON 序列化的输入。
class ToEncode {
ToEncode(this.v1, this.v2);
final String v1;
final String v2;
Object toJson() {
return [v1, v2];
}
}
print(jsonEncode(ToEncode('v1', 'v2')));
Listing 9-16Use toJson() function
如果想在序列化的 JSON 字符串中有缩进,需要直接使用JsonEncoder
类。在清单 9-17 中,两个空格被用作缩进。
String indentString = JsonEncoder.withIndent(' ').convert(data);
print(indentString);
Listing 9-17Add indent
9.8 处理复杂 JSON 数据
问题
您希望有一种类型安全的方法来处理 JSON 数据。
解决办法
使用json_annotation
和json_serializable
库。
讨论
使用dart:convert
库中的jsonEncode()
和jsonDecode()
函数可以轻松处理简单的 JSON 数据。当 JSON 数据具有复杂的结构时,使用这两个函数不是很方便。当反序列化 JSON 字符串时,结果通常是列表或映射。如果 JSON 数据有嵌套结构,那么从列表或映射中提取值就不容易了。当序列化对象时,您需要向这些类添加toJson()
方法来构建列表或映射。这些任务可以通过使用json_annotation
和json_serializable
库的代码生成来简化。
json_annotation
库提供注释来定制 JSON 序列化和反序列化行为。json_serializable
库提供了生成处理 JSON 数据的代码的构建过程。要使用这两个库,您需要将它们添加到pubspec.yaml
文件中。在清单 9-18 中,json_serializable
库被添加到dependencies
,而json_serializable
库被添加到dev_dependencies
。
dependencies:
json_annotation: ².0.0
dev_dependencies:
build_runner: ¹.0.0
json_serializable: ².0.0
Listing 9-18Add json_annotation and json_serializable
在清单 9-19 中,Person
类在json_serialize.dart
文件中。注释@JsonSerializable()
意味着为Person
类生成代码。生成的代码在json_serialize.g.dart
文件中。清单 9-19 中使用的函数_$PersonFromJson()
和_$PersonToJson()
来自生成的文件。_$PersonFromJson()
函数用于Person.fromJson()
构造函数,而_$PersonToJson()
函数用于toJson()
方法。
import 'package:json_annotation/json_annotation.dart';
part 'json_serialize.g.dart';
()
class Person {
Person({this.firstName, this.lastName, this.email});
final String firstName;
final String lastName;
final String email;
factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);
Map<String, dynamic> toJson() => _$PersonToJson(this);
}
Listing 9-19Use json_serializable
要生成代码,需要运行flutter packages pub run build_runner build
命令。清单 9-20 显示了生成的文件。
part of 'json_serialize.dart';
Person _$PersonFromJson(Map<String, dynamic> json) {
return Person(
firstName: json['firstName'] as String,
lastName: json['lastName'] as String,
email: json['email'] as String);
}
Map<String, dynamic> _$PersonToJson(Person instance) => <String, dynamic>{
'firstName': instance.firstName,
'lastName': instance.lastName,
'email': instance.email
};
Listing 9-20Generated code to handle JSON data
JsonSerializable
标注有不同的属性来定制行为;见表 9-8 。
表 9-8
JsonSerializable 的属性
|名字
|
缺省值
|
描述
|
| — | — | — |
| anyMap
| false
| 如果为 true,则使用 Map 作为地图类型;否则,使用地图。 |
| checked
| false
| 是否添加额外的检查来验证数据类型。 |
| createFactory
| true
| 是否生成将地图转换为对象的函数。 |
| createToJson
| true
| 是否生成可用作 toJson()函数的函数。 |
| disallowUnrecognizedKeys
| false
| 为 true 时,无法识别的键被视为错误;否则,它们将被忽略。 |
| explicitToJson
| false
| 为 true 时,生成的 toJson()函数对嵌套对象使用 toJson。 |
| fieldRename
| FieldRename.none
| 将类字段的名称转换成 JSON 映射键的策略。 |
| generateToJsonFunction
| true
| 为真时,生成顶层函数;否则,用 toJson()函数生成一个 mixin 类。 |
| includeIfNull
| true
| 是否包含具有空值的字段。 |
| nullable
| true
| 是否优雅地处理空值。 |
| useWrappers
| false
| 是否使用包装类在序列化过程中最大限度地减少 Map 和 List 实例的使用。 |
generateToJsonFunction
属性决定了如何生成toJson()
函数。当值为true
时,会生成类似清单 9-20 中_$PersonToJson()
的顶级函数。在清单 9-21 中,User
类的generateToJsonFunction
属性被设置为false
。
(
generateToJsonFunction: false,
)
class User extends Object with _$UserSerializerMixin {
User(this.name);
final String name;
}
Listing 9-21
User class
在清单 9-22 中,用toJson()
方法生成了_$UserSerializerMixin
类,而不是函数。清单 9-21 中的User
类只需要使用这个 mixin 类。
User _$UserFromJson(Map<String, dynamic> json) {
return User(json['name'] as String);
}
abstract class _$UserSerializerMixin {
String get name;
Map<String, dynamic> toJson() => <String, dynamic>{'name': name};
}
Listing 9-22Generated code for User class
JsonKey
注解指定了一个字段如何被序列化。表 9-9 显示了JsonKey
的属性。
表 9-9
JsonKey 的属性
|名字
|
描述
|
| — | — |
| name
| JSON 映射键。如果为 null,则使用字段名称。 |
| nullable
| 是否优雅地处理空值。 |
| includeIfNull
| 如果值为空,是否包括此字段。 |
| ignore
| 是否忽略该字段。 |
| fromJson
| 反序列化该字段的函数。 |
| toJson
| 序列化该字段的函数。 |
| defaultValue
| 用作默认值的值。 |
| required
| JSON 映射中是否需要该字段。 |
| disallowNullValue
| 是否不允许空值。 |
清单 9-23 展示了一个使用JsonKey
的例子。
(
name: 'first_name',
required: true,
includeIfNull: true,
)
final String firstName;
Listing 9-23Use JsonKey
JsonValue
注释指定用于序列化的枚举值。在清单 9-24 中,JsonValue
注释被添加到Color
的所有枚举值中。
enum Color {
('R')
Red,
('G')
Green,
('B')
Blue
}
Listing 9-24Use JsonValue
JsonLiteral
annotation 从文件中读取 JSON 数据,并将内容转换成对象。它允许轻松访问静态 JSON 数据文件的内容。在清单 9-25 中,JsonLiteral
注释被添加到data
getter 中。_$dataJsonLiteral
是 JSON 文件中数据的生成变量。
('data.json', asConst: true)
Map get data => _$dataJsonLiteral;
Listing 9-25
Use JsonLiteral
9.9 处理 XML 数据
问题
您希望在 Flutter 应用中处理 XML 数据。
解决办法
使用xml
库。
讨论
XML 是一种流行的数据交换格式。你可以使用xml
库来处理 Flutter 应用中的 XML 数据。您需要先将xml: ³.3.1
添加到pubspec.yaml
文件的dependencies
中。与 JSON 数据类似,XML 数据有两种使用场景:
-
解析 XML 文档和查询数据。
-
构建 XML 文档。
解析 XML 文档
要解析 XML 文档,您需要使用parse()
函数,该函数将一个 XML 字符串作为输入,并返回解析后的XmlDocument
对象。使用XmlDocument
对象,可以查询和遍历 XML 文档树,从中提取数据。
要查询文档树,可以使用findElements()
和findAllElements()
方法。这两个方法接受一个标记名和一个可选的名称空间作为参数,并返回一个Iterable<XmlElement>
对象。不同的是,findElements()
方法只搜索直系子代,而findAllElements()
方法搜索所有后代子代。要遍历文档树,您可以使用表 9-10 中所示的属性。
表 9-10
XmlParent 的属性
|名字
|
类型
|
描述
|
| — | — | — |
| children
| XmlNodeList<XmlNode>
| 此节点的直接子节点。 |
| ancestors
| Iterable<XmlNode>
| 文档顺序相反的此节点的祖先。 |
| descendants
| Iterable<XmlNode>
| 按文档顺序排列的此节点的后代。 |
| attributes
| List<XmlAttribute>
| 按文档顺序排列的该节点的属性节点。 |
| preceding
| Iterable<XmlNode>
| 按文档顺序位于此节点开始标记之前的节点。 |
| following
| Iterable<XmlNode>
| 按照文档顺序,此节点的结束标记后面的节点。 |
| parent
| XmlNode
| 此节点的父节点可以为空。 |
| firstChild
| XmlNode
| 此节点的第一个子节点可以为空。 |
| lastChild
| XmlNode
| 此节点的最后一个子节点可以为空。 |
| nextSibling
| XmlNode
| 此节点的下一个同级可以为空。 |
| previousSibling
| XmlNode
| 此节点的上一个同级可以为空。 |
| root
| XmlNode
| 树根。 |
在清单 9-26 中,输入的 XML 字符串(摘自 https://msdn.microsoft.com/en-us/windows/desktop/ms762271
)被解析并查询第一个book
元素。然后提取title
元素的文本和id
属性的值。
String xmlStr = "'
<?xml version="1.0"?>
<catalog>
<book id="bk101">
<Author>Gambardella, Matthew</author>
<title>XML Developer's Guide</title>
<genre>Computer</genre>
<price>44.95</price>
<publish_date>2000-10-01</publish_date>
<description>An in-depth look at creating applications
with XML.</description>
</book>
<book id="bk102">
<Author>Ralls, Kim</author>
<title>Midnight Rain</title>
<genre>Fantasy</genre>
<price>5.95</price>
<publish_date>2000-12-16</publish_date>
<description>A former architect battles corporate zombies, an evil sorceress, and her own childhood to become queen of the world.</description>
</book>
</catalog>
"';
XmlDocument document = parse(xmlStr);
XmlElement firstBook = document.rootElement.findElements('book').first;
String title = firstBook.findElements('title').single.text;
String id = firstBook.attributes
.firstWhere((XmlAttribute attr) => attr.name.local == 'id')
.value;
print('$id => $title');
Listing 9-26XML document parsing and querying
构建 XML 文档
要构建 XML 文档,可以使用XmlBuilder
类。XmlBuilder
类提供了构建 XML 文档不同组件的方法;见表 9-11 。使用这些方法,我们可以以自顶向下的方式构建 XML 文档,从根元素开始,一层一层地构建嵌套内容。
表 9-11
XmlBuilder 的方法
|名字
|
描述
|
| — | — |
| element()
| 用指定的标记名、名称空间、属性和嵌套内容创建一个XmlElement
节点。 |
| attribute()
| 用指定的名称、值、命名空间和类型创建一个XmlAttribute
节点。 |
| text()
| 用指定的文本创建一个XmlText
节点。 |
| namespace()
| 将名称空间prefix
绑定到uri
。 |
| cdata()
| 用指定的文本创建一个XmlCDATA
节点。 |
| comment()
| 用指定的文本创建一个XmlComment
节点。 |
| processing()
| 用指定的target
和text
创建一个XmlProcessing
节点。 |
构建完成后,可以使用XmlBuilder
的build()
方法来构建XmlNode
作为结果。在清单 9-27 中,根元素是一个具有id
属性的note
元素。nest
参数的值是一个使用构建器方法构建节点元素内容的函数。
XmlBuilder builder = XmlBuilder();
builder.processing('xml', 'version="1.0"');
builder.element(
'note',
attributes: {
'id': '001',
},
nest: () {
builder.element('from', nest: () {
builder.text('John');
});
builder.element('to', nest: () {
builder.text('Jane');
});
builder.element('message', nest: () {
builder
..text('Hello!')
..comment('message to send');
});
},
);
XmlNode xmlNode = builder.build();
print(xmlNode.toXmlString(pretty: true));
Listing 9-27Use XmlBuilder
清单 9-28 显示了清单 9-27 中代码构建的 XML 文档。
<?xml version="1.0"?>
<note id="001">
<from>John</from>
<to>Jane</to>
<message>Hello!
<!--message to send-->
</message>
</note>
Listing 9-28Built XML document
9.10 处理 HTML 数据
问题
你想在 Flutter 应用中解析 HTML 文档。
解决办法
使用html
库。
讨论
尽管 JSON 和 XML 数据格式在 Flutter 应用中很流行,但您可能仍然需要解析 HTML 文档来提取数据。这个过程称为屏幕抓取。你可以使用html
库来解析 HTML 文档。要使用这个库,需要将html: ⁰.13.4+1
添加到pubspec.yaml
文件的dependencies
中。
parse()
函数将 HTML 字符串解析成Document
对象。这些Document
对象可以使用 W3C DOM API 进行查询和操作。在清单 9-29 中,首先解析 HTML 字符串,然后使用getElementsByTagName()
方法获取li
元素,最后从li
元素中提取id
属性和文本。
import 'package:html/dom.dart';
import 'package:html/parser.dart' show parse;
void main() {
String htmlStr = "'
<ul>
<li id="001">John</li>
<li id="002">Jane</li>
<li id="003">Mary</li>
</ul>
"';
Document document = parse(htmlStr);
var users = document.getElementsByTagName('li').map((Element element) {
return {
'id': element.attributes['id'],
'name': element.text,
};
});
print(users);
}
Listing 9-29Parse HTML document
9.11 发送 HTTP 请求
问题
您希望向后端服务发送 HTTP 请求。
解决办法
使用dart:io
库中的HttpClient
。
讨论
HTTP 协议是公开 web 服务的流行选择。表示可以是 JSON 或 XML。通过使用来自dart:io
库的HttpClient
类,您可以轻松地通过 HTTP 与后端服务进行交互。
要使用HttpClient
类,首先需要选择一个 HTTP 方法,然后为请求准备HttpClientRequest
对象,为响应处理HttpClientResponse
对象。HttpClient
类有不同的方法对,对应不同的 HTTP 方法。例如,get()
和getUrl()
方法都用于发送 HTTP GET 请求。不同的是,get()
方法接受host
、port
和path
参数,而getUrl()
方法接受Uri
类型的url
参数。你可以看到其他对,如post()
和postUrl()
、put()
和putUrl()
、patch()
和patchUrl()
、delete()
和deleteUrl()
、head()
和headUrl()
。
这些方法返回Future<HttpClientRequest>
对象。你需要用then()
方法链接返回的Future
对象来准备HttpClientRequest
对象。例如,您可以修改 HTTP 请求头或编写请求体。then()
方法需要返回HttpClientRequest.close()
方法的值,这是一个Future<HttpClientResponse>
对象。在Future<HttpClientResponse>
对象的then()
方法中,您可以使用该对象获取响应正文、标题、cookies 和其他信息。
在清单 9-30 ,request.close()
方法在第一个then()
方法中被直接调用,因为我们不需要对HttpClientRequest
对象做任何事情。_handleResponse()
函数将 HTTP 响应解码为 UTF-8 字符串并打印出来。HttpClientResponse
类实现了Stream<List<int>>
,所以响应体可以理解为流。
void _handleResponse(HttpClientResponse response) {
response.transform(utf8.decoder).listen(print);
}
HttpClient httpClient = HttpClient();
httpClient
.getUrl(Uri.parse('https://httpbin.org/get'))
.then((HttpClientRequest request) => request.close())
.then(_handleResponse);
Listing 9-30Send HTTP GET request
如果需要用 body 发送 HTTP POST、PUT、PATCH 请求,可以用HttpClientRequest.write()
方法写 body;参见清单 9-31 。
httpClient
.postUrl(Uri.parse('https://httpbin.org/post'))
.then((HttpClientRequest request) {
request.write('hello');
return request.close();
}).then(_handleResponse);
Listing 9-31Write HTTP request body
如果需要修改 HTTP 请求头,可以使用HttpClientRequest.headers
属性修改HttpHeaders
对象;见清单 9-32 。
httpClient
.getUrl(Uri.parse('https://httpbin.org/headers'))
.then((HttpClientRequest request) {
request.headers.set(HttpHeaders.userAgentHeader, 'my-agent');
return request.close();
}).then(_handleResponse);
Listing 9-32Modify HTTP request headers
如果需要支持 HTTP 基本认证,可以使用HttpClient.addCredentials()
方法添加HttpClientBasicCredentials
对象;见清单 9-33 。
String username = 'username', password = 'password';
Uri uri = Uri.parse('https://httpbin.org/basic-auth/$username/$password');
httpClient.addCredentials(
uri, null, HttpClientBasicCredentials(username, password));
httpClient
.getUrl(uri)
.then((HttpClientRequest request) => request.close())
.then(_handleResponse);
Listing 9-33Basic authentication
9.12 连接到 WebSocket
问题
你想在 Flutter 应用中连接到 WebSocket 服务器。
解决办法
使用dart:io
库中的WebSocket
类。
讨论
WebSockets 广泛用于 web 应用中,以提供浏览器和服务器之间的双向通信。他们还可以提供后台数据的实时更新。如果您已经有了一个 WebSocket 服务器,它可以与浏览器中运行的 web 应用进行交互,您可能还希望在 Flutter 应用中提供相同的功能。dart:io
库中的WebSocket
类可以用来实现 WebSocket 连接。
静态WebSocket.connect()
方法连接到 WebSocket 服务器。您需要提供带有方案ws
或wss
的服务器 URL。您可以选择提供子协议列表和标题映射。connect()
方法的返回值是一个Future<WebSocket>
对象。WebSocket
类实现了Stream
类,所以你可以读取从服务器发送的数据流。要向服务器发送数据,可以使用add()
和addStream()
方法。
在清单 9-34 中,WebSocket 连接到演示 echo 服务器。通过使用listen()
方法订阅WebSocket
对象,我们可以处理从服务器发送的数据。两个add()
方法调用向服务器发送两条消息。
WebSocket.connect('ws://demos.kaazing.com/echo').then((WebSocket webSocket) {
webSocket.listen(print, onError: print);
webSocket.add('hello');
webSocket.add('world');
webSocket.close();
}).catchError(print);
Listing 9-34Connect to WebSocket
9.13 连接插座
问题
您想要连接到套接字服务器。
解决办法
使用dart:io
库中的Socket
类。
讨论
如果你想在 Flutter 应用中连接 socket 服务器,可以使用dart:io
库中的Socket
类。静态的Socket.connect()
方法在指定的host
和port
连接到一个套接字服务器,并返回一个Future<Socket>
对象。Socket
类实现了Stream<List<int>>
,所以你可以通过订阅流从服务器读取数据。要向服务器发送数据,可以使用add()
和addStream()
方法。
在清单 9-35 中,一个套接字服务器在端口10080
上启动。该服务器将接收到的字符串转换成大写字母,并发回结果。
import 'dart:io';
import 'dart:convert';
void main() {
ServerSocket.bind('127.0.0.1', 10080).then((serverSocket) {
serverSocket.listen((socket) {
socket.addStream(socket
.transform(utf8.decoder)
.map((str) => str.toUpperCase())
.transform(utf8.encoder));
});
});
}
Listing 9-35Simple socket server
在清单 9-36 中,Socket.connect()
方法用于连接清单 9-35 中所示的 socket 服务器。从服务器接收的数据被打印出来。两个字符串被发送到服务器。
void main() {
Socket.connect('127.0.0.1', 10080).then((socket) {
socket.transform(utf8.decoder).listen(print);
socket.write('hello');
socket.write('world');
socket.close();
});
}
Listing 9-36Connect to socket server
9.14 基于 JSON 的交互式 REST 服务
问题
您希望使用基于 JSON 的 REST 服务。
解决办法
使用HttpClient
、json_serialize
库和FutureBuilder
控件。
讨论
移动应用后端通过以 JSON 为代表的 HTTP 协议来公开服务是一种流行的选择。通过使用HttpClient
、json_serialize
库和FutureBuilder
小部件,您可以构建 UI 来使用这些 REST 服务。这个菜谱提供了一个具体的例子,它结合了清单 9-6 、 9-8 和 9-11 中的内容。
这个例子使用 GitHub Jobs API ( https://jobs.github.com/api
)获取 GitHub 网站上的工作列表。在清单 9-37 中,Job
类代表一个工作清单。在JsonSerializable
注释中,createToJson
属性被设置为false
,因为我们只需要解析来自 API 的 JSON 响应。_parseDate
函数解析 JSON 对象的created_at
字段中的字符串。你需要添加intl
库来使用DateFormat
类。
part 'github_jobs.g.dart';
DateFormat _dateFormat = DateFormat('EEE MMM dd HH:mm:ss yyyy');
DateTime _parseDate(String str) =>
_dateFormat.parse(str.replaceFirst(' UTC', "), true);
(
createToJson: false,
)
class Job {
Job();
String id;
String type;
String url;
(name: 'created_at', fromJson: _parseDate)
DateTime createdAt;
String company;
(name: 'company_url')
String companyUrl;
(name: 'company_logo')
String companyLogo;
String location;
String title;
String description;
(name: 'how-to-apply')
String howToApply;
factory Job.fromJson(Map<String, dynamic> json) => _$JobFromJson(json);
}
Listing 9-37
Job class
在清单 9-38 中,HttpClient
对象用于向 GitHub Jobs API 发送 HTTP GET 请求,并使用jsonDecode()
函数解析 JSON 响应。类型为Future<List<Job>>
的Future
对象被FutureBuilder
小部件用来构建 UI。JobsList
小部件接受一个List<Job>
对象,并使用ListView
小部件显示列表。
class GitHubJobsPage extends StatelessWidget {
final Future<List<Job>> _jobs = HttpClient()
.getUrl(Uri.parse('https://jobs.github.com/positions.json'
'?description=java&location=new+york'))
.then((HttpClientRequest request) => request.close())
.then((HttpClientResponse response) {
return response.transform(utf8.decoder).join(").then((String content) {
return (jsonDecode(content) as List<dynamic>)
.map((json) => Job.fromJson(json))
.toList();
});
});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('GitHub Jobs'),
),
body: FutureBuilder<List<Job>>(
future: _jobs,
builder: (BuildContext context, AsyncSnapshot<List<Job>> snapshot) {
if (snapshot.hasData) {
return JobsList(snapshot.data);
} else if (snapshot.hasError) {
return Center(
child: Text(
'${snapshot.error}',
style: TextStyle(color: Colors.red),
),
);
} else {
return Center(
child: CircularProgressIndicator(),
);
}
},
),
);
}
}
class JobsList extends StatelessWidget {
JobsList(this.jobs);
final List<Job> jobs;
Widget build(BuildContext context) {
return ListView.separated(
itemBuilder: (BuildContext context, int index) {
Job job = jobs[index];
return ListTile(
title: Text(job.title),
subtitle: Text(job.company),
);
},
separatorBuilder: (BuildContext context, int index) {
return Divider();
},
itemCount: jobs.length,
);
}
}
Listing 9-38Widget to show jobs
9.15 与 gRPC 服务互动
问题
您希望与 gRPC 服务进行交互。
解决办法
使用grpc
库。
讨论
gRPC ( https://grpc.io/
)是一个高性能、开源的通用 RPC 框架。这个菜谱展示了如何与 gRPC 服务交互。要交互的 gRPC 服务是来自 gRPC 官方示例的 greeter 服务( https://github.com/grpc/grpc/tree/master/examples/node
)。您需要首先启动 gRPC 服务器。
要在 Flutter 应用中使用这个 gRPC 服务,需要先安装协议缓冲编译器( https://github.com/protocolbuffers/protobuf
)。在为您的平台下载发布文件并提取其内容后,您需要将提取的bin
目录添加到PATH
环境变量中。您可以运行protoc --version
命令来验证安装。这个配方使用的版本是3.7.1
。
还需要安装 Dart protoc 插件( https://github.com/dart-lang/protobuf/tree/master/protoc_plugin
)。最简单的安装方法是运行以下命令。
$ flutter packages pub global activate protoc_plugin
因为我们使用flutter packages
来运行安装,所以二进制文件放在 Flutter SDK 的.pub-cache/bin
目录下。你需要把这个路径添加到PATH
环境变量中。插件要求dart
命令可用,所以你还需要将 Flutter SDK 的bin/cache/dart-sdk/bin
目录添加到 PATH 环境变量中。现在我们可以使用protoc
来生成 Dart 文件,以便与欢迎服务进行交互。在下面的命令中,lib/grpc/generated
是生成文件的输出路径。proto_file_path
是 proto 文件的路径。helloworld.proto
文件包含迎宾服务的定义。库protobuf
和grpc
也需要添加到pubspec.yaml
文件的dependencies
中。
$ protoc --dart_out=grpc:lib/grpc/generated --proto_path=<proto_file_path> <proto_file_path>/helloworld.proto
生成的helloworld.pbgrpc.dart
文件提供了GreeterClient
类来与服务交互。在清单 9-39 中,创建了一个ClientChannel
来连接 gRPC 服务器。创建GreeterClient
对象时需要通道。sayHello()
方法向服务器发送请求并接收响应。
import 'package:grpc/grpc.dart';
import 'generated/helloworld.pbgrpc.dart';
void main() async {
final channel = new ClientChannel('localhost',
port: 50051,
options: const ChannelOptions(
credentials: const ChannelCredentials.insecure()));
final stub = new GreeterClient(channel);
try {
var response = await stub.sayHello(new HelloRequest()..name = 'John');
print('Received: ${response.message}');
} catch (e) {
print('Caught error: $e');
}
await channel.shutdown();
}
Listing 9-39Interact with gRPC service
9.16 摘要
本章主要关注与后端服务交互的不同方式,包括 HTTP、WebSocket、Socket 和 gRPC。期货和流在异步计算中起着重要的作用。本章还讨论了如何处理 JSON、XML 和 HTML 数据。在下一章,我们将讨论 Flutter 应用中的状态管理。