使用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()方法将当前路线弹出导航器。其实在使用Scaffoldwidget的时候,应用栏里自动添加了一个后退按钮,所以不需要使用显式的后退按钮。
您想要从不同的地方导航到相同的路线。
使用带有Navigator.pushNamed()方法的命名路线。
当使用Navigator.push()方法将新路线推送到导航器时,使用构建器函数按需构建新路线。当路线可以从不同的地方导航时,这种方法不太适用,因为我们不想重复构建路线的代码。在这种情况下,使用命名路由是更好的选择。命名路由具有唯一的名称。Navigator.pushNamed()方法使用名称来指定要推送到导航器的路线。
在清单8-2中,按下“注册”按钮将指定的路线/sign_up推送到导航器。
classLogInPageextendsStatelessWidget{@overrideWidgetbuild(BuildContextcontext){returnScaffold(appBar:AppBar(title:Text('LogIn'),),body:Center(child:RaisedButton(child:Text('SignUp'),onPressed:(){Navigator.pushNamed(context,'/sign_up');},),),);}}Listing8-2Usenamedroute在清单8-3中,在routes参数中注册了两条命名路径。
classPageNavigationAppextendsStatelessWidget{@overrideWidgetbuild(BuildContextcontext){returnMaterialApp(title:'PageNavigation',home:IndexPage(),routes:{'/sign_up':(context)=>SignUpPage(),'/log_in':(context)=>LogInPage(),},);}}Listing8-3Registernamedroutes8.3在路线之间传递数据问题您希望在不同的路由之间传递数据。
使用构造函数参数或RouteSettings对象将数据传递给路由,使用Navigator.pop()方法的result参数从路由传递数据。
构建路径内容时,路径可能需要额外的数据。弹出时,路由也可能返回一些数据。例如,编辑用户详细信息的路由可能需要当前的详细信息作为输入,并返回更新的详细信息作为输出。根据导航路线的方式,有不同的方法在路线之间传递数据。
在清单8-4中,UserDetails类包含用户的名和姓。UserDetailsPage显示用户的详细信息。当按下编辑按钮时,一条新路线被推送到导航器。新路由的内容是一个EditUserDetailsPage小部件,将UserDetails对象作为构造函数参数。新路由的返回值也是一个UserDetails对象,用于更新UserDetailsPage的状态。
UserDetailsresult=awaitNavigator.pushNamed(context,'/edit_user',arguments:_userDetails,);Listing8-6Passdatatonamedroute被命名的路线/edit_user被登记在MaterialApp中。不能使用route参数,因为您不能访问在构建器函数中传递给路线的数据。应使用WidgetsApp、MaterialApp或CupertinoApp的onGenerateRoute参数。onGenerateRoute参数的类型为RouteFactory,是函数类型Route(RouteSettingssettings)的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对象。
使用onGenerateRoute参数。
当使用WidgetsApp的routes参数注册命名路线时,只有整个路线名称可用于匹配Route对象。如果想用复杂的逻辑将Route对象与路线名称匹配,可以使用onGenerateRoute参数和RouteSettings对象。例如,您可以将所有以/order开头的路线名称匹配到一个Route对象。
在清单8-8中,所有以/order开头的路线名称将使用OrderPage导航到一条路线。
MaterialApp(onGenerateRoute:(RouteSettingssettings){if(settings.name.startsWith('/order')){returnMaterialPageRoute(settings:settings,builder:(context){returnOrderPage();},);}},);Listing8-8Routematching8.5处理未知路线问题您希望处理导航到未知路线的情况。
使用Navigator、WidgetsApp、MaterialApp、CupertinoApp的onUnknownRoute参数。
可能会要求导航员导航到未知的路线。这可能是由于应用中的编程错误或外部路线导航请求造成的。如果onGenerateRoute函数为给定的RouteSettings对象返回null,则onUnknownRoute函数被调用以提供一条回退路线。这个onUnknownRoute函数通常用于错误处理,就像web应用中的404页面一样。onUnknownRoute的类型也是RouteFactory。
在清单8-9中,onUnknownRoute函数返回显示NotFoundPage小部件的路线。
MaterialApp(onUnknownRoute:(RouteSettingssettings){returnMaterialPageRoute(settings:settings,builder:(BuildContextcontext){returnNotFoundPage(settings.name);},);},);Listing8-9UseonUnknownRoute8.6显示材质设计对话框问题您希望显示材质设计对话框。
使用showDialog()功能和Dialog、SimpleDialog和AlertDialog控件。
要使用材质设计对话框,你需要创建对话框部件并显示它们。Dialog类及其子类SimpleDialog和AlertDialog可以用来创建对话框。
SimpleDialog小工具为用户提供了几个选项。选项使用SimpleDialogOption类表示。一个SimpleDialogOption小部件可以有一个子小部件和一个onPressed回调。当创建SimpleDialog时,你可以提供一个孩子列表和一个可选的标题。AlertDialogwidget向用户呈现内容和动作列表。AlertDialog用于确认用户或要求确认。
在清单8-10中,按下按钮会显示一个带有两个选项的简单对话框。
图8-1
材质设计简单对话框
在清单8-11中,按下按钮会显示一个带有两个动作的警告对话框。
图8-2
材质设计警告对话框
您想要显示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|此操作是否为默认操作。||isdstructuralaction|bool|这个动作是否具有破坏性。破坏性的行为有不同的风格。||textStyle|TextStyle|应用于操作的文本样式。|
在清单8-12中,按下按钮会显示一个iOS风格的警告对话框。
图8-3
iOS警报对话框
如果你想创建一个自定义对话框,你可以使用CupertinoPopupSurface小部件来创建圆角矩形表面。
您想要在iOS应用中呈现一组操作供用户选择。
使用showCupertinoModalPopup()功能和CupertinoActionSheet控件。
如果想在iOS应用中呈现一组动作供用户选择,可以使用showCupertinoModalPopup()函数显示CupertinoActionSheetwidgets。一个CupertinoActionSheet可以有标题、消息、取消按钮和动作列表。动作被表示为CupertinoActionSheetAction小部件。CupertinoActionSheetAction构造器有参数child、onPressed、isDefaultAction、isDestructiveAction,与表8-2中的CupertinoDialogAction构造器含义相同。
在清单8-13中,按下按钮会显示一个带有三个动作和一个取消按钮的动作表。
图8-4
iOS行动表
你想在材质设计应用中显示菜单。
使用showMenu()函数和PopupMenuEntry类的实现。
要使用showMenu()函数,你需要有一个PopupMenuEntry对象的列表。有不同类型的PopupMenuEntry实现:
PopupMenuItem是具有其值的类型的类属。表8-3显示了PopupMenuItem构造器的参数。CheckedPopupMenuItem是PopupMenuItem的子类。CheckedPopupMenuItem有checked属性来指定是否显示复选标记。
表8-3
PopupMenuItem构造函数的参数
||---|---|---||child|Widget|菜单项的内容。||value|T|菜单项的值。||enabled|bool|是否可以选择此菜单项。||height|double|菜单项的高度。默认为48。|
表8-4
showMenu()的参数
清单8-14中的菜单包含一个PopupMenuItem,一个PopupMenuDivider,和一个CheckedPopupMenuItem。
表8-5
弹出菜单按钮的参数
清单8-15展示了如何使用PopupMenuButton来实现与清单8-14中相同的菜单。
图8-5
材质设计菜单
你想要复杂的页面流。
使用嵌套的Navigator实例。
一个Navigator实例管理它自己的路由栈。对于简单的app,一个Navigator实例一般就够了,可以简单使用WidgetsApp、MaterialApp或者CupertinoApp创建的Navigator实例。如果你的应用有复杂的页面流,你可能需要使用嵌套导航器。由于Navigator本身也是一个小部件,Navigator实例可以像普通小部件一样创建。由WidgetsApp、MaterialApp或CupertinoApp创建的Navigator实例成为根导航器。所有导航器都是按层次结构组织的。要获得根导航器,可以在调用Navigator.of()方法时将rootNavigator参数设置为true。表8-6显示了Navigator构造器的参数。
表8-6
导航器参数
在清单8-16中,导航器有两条命名的路线。初始路线设置为on_boarding/topic,所以先显示UserOnBoardingTopicPage。
classUserOnBoardingPageextendsStatelessWidget{@overrideWidgetbuild(BuildContextcontext){returnScaffold(appBar:AppBar(title:Text('GetStarted'),),body:Navigator(initialRoute:'on_boarding/topic',onGenerateRoute:(RouteSettingssettings){WidgetBuilderbuilder;switch(settings.name){case'on_boarding/topic':builder=(BuildContextcontext){returnUserOnBoardingTopicPage();};break;case'on_boarding/follower':builder=(BuildContextcontext){returnUserOnBoardingFollowPage();};break;}returnMaterialPageRoute(builder:builder,settings:settings,);},),);}}Listing8-16Useron-boardingpage在清单8-17中,按下“下一步”按钮导航到路线名称为on_boarding/follower的下一步。按下“完成”按钮,使用根导航器弹出登机页面。
图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实例。
您希望在导航状态改变时得到通知。
使用NavigatorObserver。
有时,您可能希望在导航器状态改变时得到通知。例如,您希望分析使用应用的用户的页面流量,以改善用户体验。当创建Navigator实例时,您可以提供一个NavigatorObserver对象的列表,作为导航器状态变化的观察者。表8-7显示了NavigatorObserver接口的方法。
表8-7
导航观测方法
||---|---||didPop(Routeroute,RoutepreviousRoute)|弹出route并且previousRoute是新激活的路由。||didPush(Routeroute,RoutepreviousRoute)|按下route,而previousRoute是先前激活的路线。||didRemove(Routeroute,RoutepreviousRoute)|route被删除,previousRoute是被删除路线的下一条路线。||didReplace(RoutenewRoute,RouteoldRoute)|将oldRoute替换为newRoute。||didStartUserGesture(Routeroute,RoutepreviousRoute)|用户开始使用手势移动route。路线正下方的路线是previousRoute。||didStopUserGesture()|用户使用手势停止移动路线。|
在清单8-18中,LoggingNavigatorObserver类记录路由推送和弹出时的消息。
表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对象被释放时,订阅被删除。
将WillPopCallback与ModalRoute对象一起使用。
当一条路线被推送到导航器时,可以使用Scaffold中的后退按钮或Android中的系统后退按钮弹出该路线。有时,您可能想要阻止路线被弹出。例如,如果页面中有未保存的更改,您可能希望首先显示一个警告对话框来要求确认。当使用Navigator.maybePop()方法而不是Navigator.pop()方法时,您有机会决定弹出路线的请求是否应该继续。
你想处理Future物体。
使用then()和catchError()方法处理Future对象的结果。
给定一个Future对象,关于其结果有三种不同的情况:
要注册对Future对象的回调,可以使用then()方法注册一个值回调和一个可选的错误回调,或者使用catchError()方法只注册一个错误回调。建议使用then()方法只注册一个值回调。这是因为如果一个错误回调是使用then()方法的onError参数注册的,这个错误回调不能处理在值回调中抛出的错误。大多数情况下,您希望错误回调处理所有可能的错误。如果一个Future对象的错误没有被它的错误回调函数处理,这个错误将被全局处理程序处理。
在清单9-1中,Future对象可能以值1或一个Error对象结束。值和错误回调都被注册来处理结果。
Future.delayed(Duration(seconds:1),(){if(Random().nextBool()){return1;}else{throwError();}},).then((value){print(value);}).catchError((error){print('error:$error');});Listing9-1Usethen()andcatchError()methodstohandleresultthen()和catchError()方法的返回值也是Future对象。给定一个Future对象A,调用A.then(func)的结果是另一个Future对象B,如果func回调运行成功,FutureB将以调用func函数的返回值完成。否则,FutureB将会以调用func函数时抛出的错误完成。调用B.catchError(errorHandler)返回一个新的Future对象c。错误处理程序可以处理在FutureB中抛出的错误,这些错误可能会在未来的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));Listing9-2Chainedthen()methods如果你想在未来完成时调用函数,你可以使用whenComplete()方法。当这个future完成时,使用whenComplete()添加的函数被调用,不管它完成时是有值还是有错误。whenComplete()方法相当于其他编程语言中的finally块。then().catchError().whenComplete()的链条相当于“尝试-捕捉-最终”。
清单9-3展示了一个使用whenComplete()方法的例子。
Future.delayed(Duration(seconds:5),()=>1).timeout(Duration(seconds:2),onTimeout:()=>10,).then((value)=>print(value));Listing9-4Usetimeout()method9.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构造函数Future()、Future.delayed()、Future.sync()、Future.value()和Future.error()创建Future对象。
如果需要创建Future对象,可以使用它的构造函数Future()、Future.delayed()、Future.sync()、Future.value()和Future.error():
清单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);Listing9-7CreateFutureobjects9.4使用流问题你想处理一连串的事件。
要从流中接收事件,可以使用listen()方法来设置监听器。listen()方法的返回值是一个代表活动订阅的StreamSubscription对象。根据流上允许的订阅数量,有两种类型的流:
给定一个Stream对象,属性isBroadcast可以用来检查它是否是一个广播流。您可以使用asBroadcastStream()方法从单一订阅流创建广播流。
表9-1显示了listen()方法的参数。您可以为不同的事件提供任意数量的处理程序,并忽略那些不感兴趣的事件。
表9-1
listen()方法的参数
||---|---|---||onData|void(Tevent)|数据事件的处理程序。||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,);Listing9-8Uselisten()method使用由listen()方法返回的StreamSubscription对象,您可以管理订阅。表9-2展示了StreamSubscription类的方法。
表9-2
流订阅的方法
||---|---||cancel()|取消此订阅。||pause([FutureresumeSignal])|请求流暂停事件发出。如果提供了resumeSignal,流将在未来完成时恢复。||resume()|暂停后继续流。||onData()|替换数据事件处理程序。||onError()|替换错误事件处理程序。||onDone()|替换done事件处理程序。||asFuture([EfutureValue])|返回处理流完成的未来值。|
当您想要处理流的完成时,asFuture()方法很有用。因为一个流可以正常完成,也可以出错,所以使用这个方法会覆盖现有的onDone和onError回调。在发生错误事件的情况下,订阅被取消,返回的Future对象完成时出现错误。在完成事件的情况下,Future对象以给定的futureValue结束。
stream的强大之处在于对流应用各种转换来获得另一个流或值。表9-3显示了返回另一个Stream对象的Stream类中的方法。
表9-3
流转换
清单9-9展示了使用流转换的例子。每个语句下面的代码显示了执行的结果。
Stream.fromIterable([1,2,3]).asyncExpand((intvalue){returnStream.fromIterable([value*5,value*10]);}).listen(print);//->5,10,10,20,15,30Stream.fromIterable([1,2,3]).expand((intvalue){return[value*5,value*10];}).listen(print);//->5,10,10,20,15,30Stream.fromIterable([1,2,3]).asyncMap((intvalue){returnFuture.delayed(Duration(seconds:1),()=>value*10);}).listen(print);//->10,20,30Stream.fromIterable([1,2,3]).map((value)=>value*10).listen(print);//->10,20,30Stream.fromIterable([1,1,2]).distinct().listen(print);//->1,2Stream.fromIterable([1,2,3]).skip(1).listen(print);//->2,3Stream.fromIterable([1,2,3]).skipWhile((value)=>value%2==1).listen(print);//->2,3Stream.fromIterable([1,2,3]).take(1).listen(print);//->1Stream.fromIterable([1,2,3]).takeWhile((value)=>value%2==1).listen(print);//->1Stream.fromIterable([1,2,3]).where((value)=>value%2==1).listen(print);//->1,3Listing9-9Streamtransformations在Stream类中有其他方法返回一个Future对象;见表9-4。这些操作返回单个值,而不是流。
表9-4
单一值的方法
清单9-10显示了使用表9-4中方法的例子。每个语句下面的代码显示了执行的结果。
Stream.fromIterable([1,2,3]).forEach(print);//->1,2,3Stream.fromIterable([1,2,3]).contains(1).then(print);//->trueStream.fromIterable([1,2,3]).any((value)=>value%2==0).then(print);//->trueStream.fromIterable([1,2,3]).every((value)=>value%2==0).then(print);//->falseStream.fromIterable([1,2,3]).fold(0,(v1,v2)=>v1+v2).then(print);//->6Stream.fromIterable([1,2,3]).reduce((v1,v2)=>v1*v2).then(print);//->6Stream.fromIterable([1,2,3]).firstWhere((value)=>value%2==1).then(print);//->1Stream.fromIterable([1,2,3]).lastWhere((value)=>value%2==1).then(print);//->3Stream.fromIterable([1,2,3]).singleWhere((value)=>value%2==1).then(print);//->Unhandledexception:Badstate:ToomanyelementsListing9-10MethodsreturnFutureobjects9.5创建流问题你想要创建Stream对象。
使用不同的Stream构造函数。
有不同的Stream构造函数来创建Stream对象:
清单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),(intcount)=>count*2).take(5).listen(print);Listing9-11UseStreamconstructors另一种创建流的方法是使用StreamController类。一个StreamController对象可以向它控制的流发送不同的事件。默认的StreamController()构造器创建一个单一订阅流,而StreamController.broadcast()构造器创建一个广播流。使用StreamController,您可以以编程方式在流中生成元素。
在清单9-12中,不同的事件被发送到由StreamController对象控制的流中。
表9-5
StreamBuilder的参数
表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|连接到终止的异步计算。|
使用StreamBuilderwidget构建UI时,典型的方式是根据连接状态返回不同的widget。例如,如果连接状态正在等待,则可以返回进程指示符。
在清单9-13中,流有五个每秒生成的元素。如果连接状态为none或waiting,则返回一个CircularProgressIndicator小工具。如果状态为active或done,则根据data和error属性的值返回一个Text小工具。
在清单9-14中,我们使用了一种不同的方式来构建UI。使用hasData和hasError属性来检查状态,而不是检查连接状态。
使用dart:convert库中的jsonEncode()和jsonDecode()函数。
JSON是一种流行的web服务数据格式。为了与后端服务交互,您可能需要在两种情况下处理JSON数据:
对于这两种场景,如果您只是偶尔需要处理简单的JSON数据,那么使用dart:convert库中的jsonEncode()和jsonDecode()函数是一个不错的选择。jsonEncode()函数将镖对象转换成字符串,而jsonDecode()函数将字符串转换成镖对象。在清单9-15中,数据对象首先被序列化为JSON字符串,然后JSON字符串再次被反序列化为Dart对象。
vardata={'name':'Test','count':100,'valid':true,'list':[1,2,{'nested':'a','value':123,},],};Stringstr=jsonEncode(data);print(str);Objectobj=jsonDecode(str);print(obj);Listing9-15HandleJSONdatadart:convert库中的JSON编码器只支持有限数量的数据类型,包括数字、字符串、布尔、null、列表和带字符串键的映射。要对其他类型的对象进行编码,您需要使用toEncodable参数来提供一个函数,该函数首先将对象转换为可编码的值。默认的toEncodable函数调用对象上的toJson()方法。向需要序列化为JSON字符串的自定义类添加toJson()方法是一种常见的做法。
在清单9-16中,ToEncode类的toJson()方法返回一个列表,该列表将作为JSON序列化的输入。
classToEncode{ToEncode(this.v1,this.v2);finalStringv1;finalStringv2;ObjecttoJson(){return[v1,v2];}}print(jsonEncode(ToEncode('v1','v2')));Listing9-16UsetoJson()function如果想在序列化的JSON字符串中有缩进,需要直接使用JsonEncoder类。在清单9-17中,两个空格被用作缩进。
StringindentString=JsonEncoder.withIndent('').convert(data);print(indentString);Listing9-17Addindent9.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:2.0.0dev_dependencies:build_runner:1.0.0json_serializable:2.0.0Listing9-18Addjson_annotationandjson_serializable在清单9-19中,Person类在json_serialize.dart文件中。注释@JsonSerializable()意味着为Person类生成代码。生成的代码在json_serialize.g.dart文件中。清单9-19中使用的函数_$PersonFromJson()和_$PersonToJson()来自生成的文件。_$PersonFromJson()函数用于Person.fromJson()构造函数,而_$PersonToJson()函数用于toJson()方法。
表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。
@JsonSerializable(generateToJsonFunction:false,)classUserextendsObjectwith_$UserSerializerMixin{User(this.name);finalStringname;}Listing9-21Userclass在清单9-22中,用toJson()方法生成了_$UserSerializerMixin类,而不是函数。清单9-21中的User类只需要使用这个mixin类。
表9-9
JsonKey的属性
||---|---||name|JSON映射键。如果为null,则使用字段名称。||nullable|是否优雅地处理空值。||includeIfNull|如果值为空,是否包括此字段。||ignore|是否忽略该字段。||fromJson|反序列化该字段的函数。||toJson|序列化该字段的函数。||defaultValue|用作默认值的值。||required|JSON映射中是否需要该字段。||disallowNullValue|是否不允许空值。|
清单9-23展示了一个使用JsonKey的例子。
@JsonKey(name:'first_name',required:true,includeIfNull:true,)finalStringfirstName;Listing9-23UseJsonKeyJsonValue注释指定用于序列化的枚举值。在清单9-24中,JsonValue注释被添加到Color的所有枚举值中。
enumColor{@JsonValue('R')Red,@JsonValue('G')Green,@JsonValue('B')Blue}Listing9-24UseJsonValueJsonLiteralannotation从文件中读取JSON数据,并将内容转换成对象。它允许轻松访问静态JSON数据文件的内容。在清单9-25中,JsonLiteral注释被添加到datagetter中。_$dataJsonLiteral是JSON文件中数据的生成变量。
@JsonLiteral('data.json',asConst:true)Mapgetdata=>_$dataJsonLiteral;Listing9-25UseJsonLiteral9.9处理XML数据问题您希望在Flutter应用中处理XML数据。
使用xml库。
XML是一种流行的数据交换格式。你可以使用xml库来处理Flutter应用中的XML数据。您需要先将xml:3.3.1添加到pubspec.yaml文件的dependencies中。与JSON数据类似,XML数据有两种使用场景:
要解析XML文档,您需要使用parse()函数,该函数将一个XML字符串作为输入,并返回解析后的XmlDocument对象。使用XmlDocument对象,可以查询和遍历XML文档树,从中提取数据。
表9-10
XmlParent的属性
表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参数的值是一个使用构建器方法构建节点元素内容的函数。
XmlBuilderbuilder=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('messagetosend');});},);XmlNodexmlNode=builder.build();print(xmlNode.toXmlString(pretty:true));Listing9-27UseXmlBuilder清单9-28显示了清单9-27中代码构建的XML文档。
使用html库。
尽管JSON和XML数据格式在Flutter应用中很流行,但您可能仍然需要解析HTML文档来提取数据。这个过程称为屏幕抓取。你可以使用html库来解析HTML文档。要使用这个库,需要将html:.13.4+1添加到pubspec.yaml文件的dependencies中。
parse()函数将HTML字符串解析成Document对象。这些Document对象可以使用W3CDOMAPI进行查询和操作。在清单9-29中,首先解析HTML字符串,然后使用getElementsByTagName()方法获取li元素,最后从li元素中提取id属性和文本。
使用dart:io库中的HttpClient。
HTTP协议是公开web服务的流行选择。表示可以是JSON或XML。通过使用来自dart:io库的HttpClient类,您可以轻松地通过HTTP与后端服务进行交互。
要使用HttpClient类,首先需要选择一个HTTP方法,然后为请求准备HttpClientRequest对象,为响应处理HttpClientResponse对象。HttpClient类有不同的方法对,对应不同的HTTP方法。例如,get()和getUrl()方法都用于发送HTTPGET请求。不同的是,get()方法接受host、port和path参数,而getUrl()方法接受Uri类型的url参数。你可以看到其他对,如post()和postUrl()、put()和putUrl()、patch()和patchUrl()、delete()和deleteUrl()、head()和headUrl()。
使用dart:io库中的WebSocket类。
WebSockets广泛用于web应用中,以提供浏览器和服务器之间的双向通信。他们还可以提供后台数据的实时更新。如果您已经有了一个WebSocket服务器,它可以与浏览器中运行的web应用进行交互,您可能还希望在Flutter应用中提供相同的功能。dart:io库中的WebSocket类可以用来实现WebSocket连接。
在清单9-34中,WebSocket连接到演示echo服务器。通过使用listen()方法订阅WebSocket对象,我们可以处理从服务器发送的数据。两个add()方法调用向服务器发送两条消息。
WebSocket.connect('ws://demos.kaazing.com/echo').then((WebSocketwebSocket){webSocket.listen(print,onError:print);webSocket.add('hello');webSocket.add('world');webSocket.close();}).catchError(print);Listing9-34ConnecttoWebSocket9.13连接插座问题您想要连接到套接字服务器。
使用dart:io库中的Socket类。
在清单9-35中,一个套接字服务器在端口10080上启动。该服务器将接收到的字符串转换成大写字母,并发回结果。
import'dart:io';import'dart:convert';voidmain(){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));});});}Listing9-35Simplesocketserver在清单9-36中,Socket.connect()方法用于连接清单9-35中所示的socket服务器。从服务器接收的数据被打印出来。两个字符串被发送到服务器。
voidmain(){Socket.connect('127.0.0.1',10080).then((socket){socket.transform(utf8.decoder).listen(print);socket.write('hello');socket.write('world');socket.close();});}Listing9-36Connecttosocketserver9.14基于JSON的交互式REST服务问题您希望使用基于JSON的REST服务。
使用HttpClient、json_serialize库和FutureBuilder控件。
移动应用后端通过以JSON为代表的HTTP协议来公开服务是一种流行的选择。通过使用HttpClient、json_serialize库和FutureBuilder小部件,您可以构建UI来使用这些REST服务。这个菜谱提供了一个具体的例子,它结合了清单9-6、9-8和9-11中的内容。
使用grpc库。
$flutterpackagespubglobalactivateprotoc_plugin因为我们使用flutterpackages来运行安装,所以二进制文件放在FlutterSDK的.pub-cache/bin目录下。你需要把这个路径添加到PATH环境变量中。插件要求dart命令可用,所以你还需要将FlutterSDK的bin/cache/dart-sdk/bin目录添加到PATH环境变量中。现在我们可以使用protoc来生成Dart文件,以便与欢迎服务进行交互。在下面的命令中,lib/grpc/generated是生成文件的输出路径。proto_file_path是proto文件的路径。helloworld.proto文件包含迎宾服务的定义。库protobuf和grpc也需要添加到pubspec.yaml文件的dependencies中。