Flutter 秘籍(四)

原文:Flutter Recipes

协议:CC BY-NC-SA 4.0

八、页面导航

Flutter 应用可能有多个屏幕或页面。页面是一组功能。用户在不同的页面之间导航以使用不同的功能。像页面这样的概念在 Flutter 中被称为路由。路由不仅包括全屏页面,还包括模态对话框和弹出窗口。路线由Navigator小工具管理。本章讨论与 Flutter 中页面导航相关的方法。

8.1 实现基本页面导航

问题

您需要基本的页面导航支持。

解决办法

使用Navigator.push()导航到新路线,使用Navigator.pop()导航到以前的路线。

讨论

路线由Navigator小工具管理。导航器管理一堆路线。可以使用push()方法将路由推入堆栈,使用pop()方法将路由弹出堆栈。堆栈中的顶部元素是当前活动的路由。Navigator是一个有状态小部件,其状态为NavigatorState。要与 navigator 交互,可以使用 Navigator 的静态方法或获取一个NavigatorState的实例。通过使用Navigator.of()方法,您可以获得给定构建上下文的最近的封闭NavigatorState实例。您可以显式创建Navigator小部件,但是大多数时候您将使用由WidgetsAppMaterialAppCupertinoApp小部件创建的Navigator小部件。

使用抽象Route类的实现来表示路线。例如,PageRoute代表全屏模式路线,PopupRoute代表在当前路线上叠加一个小工具的模式路线。PageRoutePopupRoute类都是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()方法使用名称来指定要推送到导航器的路线。

命名的路由需要先注册,然后才能导航到。注册命名路径最简单的方法是使用WidgetsAppMaterialAppCupertinoApp构造函数的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参数,因为您不能访问在构建器函数中传递给路线的数据。应使用WidgetsAppMaterialAppCupertinoApponGenerateRoute参数。onGenerateRoute参数的类型为RouteFactory,是函数类型Route (RouteSettings settings)的 typedef。RouteSettings类包含创建Route对象时可能需要的数据。表 8-1 显示了RouteSettings类的属性。

表 8-1

路由设置的属性

|

名字

|

类型

|

描述

|
| — | — | — |
| name | String | 路线的名称。 |
| arguments | Object | 传递给路由的数据。 |
| isInitialRoute | bool | 此路线是否是推送到导航器的第一条路线。 |

当实现onGenerateRoute函数时,需要根据提供的RouteSettings对象返回路线。在清单 8-7 中,首先检查name属性,然后返回一个内容为EditUserDetailsPageMeterialPageRouteRouteSettingsarguments属性在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参数。

讨论

当使用WidgetsApproutes参数注册命名路线时,只有整个路线名称可用于匹配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 处理未知路线

问题

您希望处理导航到未知路线的情况。

解决办法

使用NavigatorWidgetsAppMaterialAppCupertinoApponUnknownRoute参数。

讨论

可能会要求导航员导航到未知的路线。这可能是由于应用中的编程错误或外部路线导航请求造成的。如果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()功能和DialogSimpleDialogAlertDialog控件。

讨论

要使用材质设计对话框,你需要创建对话框部件并显示它们。Dialog类及其子类SimpleDialogAlertDialog可以用来创建对话框。

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()功能和CupertinoAlertDialogCupertinoPopupSurface控件。

讨论

对于 iOS 应用,你可以使用showCupertinoDialog()功能和CupertinoAlertDialogCupertinoPopupSurface等小工具来显示对话框。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构造器有参数childonPressedisDefaultActionisDestructiveAction,与表 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构造器的参数。CheckedPopupMenuItemPopupMenuItem的子类。CheckedPopupMenuItemchecked属性来指定是否显示复选标记。

表 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实例一般就够了,可以简单使用WidgetsAppMaterialApp或者CupertinoApp创建的Navigator实例。如果你的应用有复杂的页面流,你可能需要使用嵌套导航器。由于Navigator本身也是一个小部件,Navigator实例可以像普通小部件一样创建。由WidgetsAppMaterialAppCupertinoApp创建的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时,可以提供routesonGenerateRouteonUnknownRoutenavigatorObservers参数。这些参数用于配置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对象的通知,您需要使用RouteObserversubscribe()方法将一个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 阻止路线弹出

问题

您希望阻止路线弹出导航器。

解决办法

WillPopCallbackModalRoute对象一起使用。

讨论

当一条路线被推送到导航器时,可以使用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对象,就像它们是同步的一样。

解决办法

使用asyncawait

讨论

对象代表异步计算。使用Future对象的通常方式是注册回调来处理结果。这种基于回调的风格可能会给习惯同步操作的开发人员造成障碍。使用asyncawait是 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()方法很有用。因为一个流可以正常完成,也可以出错,所以使用这个方法会覆盖现有的onDoneonError回调。在发生错误事件的情况下,订阅被取消,返回的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>小工具。

讨论

给定一个SteamFuture对象,您可能想要构建一个基于其中的数据更新其内容的小部件。您可以使用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 中,流有五个每秒生成的元素。如果连接状态为nonewaiting,则返回一个CircularProgressIndicator小工具。如果状态为activedone,则根据dataerror属性的值返回一个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。使用hasDatahasError属性来检查状态,而不是检查连接状态。

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_annotationjson_serializable库。

讨论

使用dart:convert库中的jsonEncode()jsonDecode()函数可以轻松处理简单的 JSON 数据。当 JSON 数据具有复杂的结构时,使用这两个函数不是很方便。当反序列化 JSON 字符串时,结果通常是列表或映射。如果 JSON 数据有嵌套结构,那么从列表或映射中提取值就不容易了。当序列化对象时,您需要向这些类添加toJson()方法来构建列表或映射。这些任务可以通过使用json_annotationjson_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() | 用指定的targettext创建一个XmlProcessing节点。 |

构建完成后,可以使用XmlBuilderbuild()方法来构建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()方法接受hostportpath参数,而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 服务器。您需要提供带有方案wswss的服务器 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()方法在指定的hostport连接到一个套接字服务器,并返回一个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 服务。

解决办法

使用HttpClientjson_serialize库和FutureBuilder控件。

讨论

移动应用后端通过以 JSON 为代表的 HTTP 协议来公开服务是一种流行的选择。通过使用HttpClientjson_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文件包含迎宾服务的定义。库protobufgrpc也需要添加到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 应用中的状态管理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值