Flutter—使用网络请求的页面搭建流程、State生命周期、一些组件的应用
使用网络请求的页面搭建流程
在开发APP时,我们常常会遇到如下场景:进入一个页面后,要先进行网络调用,然后使用调用返回的数据进行页面渲染。
这种页面搭建流程大致为:调用网络请求,获得json格式的数据—解析获得的数据为Dart类 — 将Dart数据传回UI。在返回数据前,可以在页面先放置一个加载动画;获得数据后,使用数据进行进行页面重绘。
网络请求
Flutter的网络请求常常使用的库有http
,dio
,其中dio
是一个强大的Dart Http请求库,支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时、自定义适配器等。dio的详细介绍与使用方法可以参见github-dio官方文档。
-
在pubspec.yaml中引入包依赖
dependencies: dio: ^3.x.x // 请使用pub上3.0.0分支的最新版本
-
极简示例
import 'package:dio/dio.dart'; void getHttp() async { try { Response response = await Dio().get("http://www.baidu.com"); print(response); } catch (e) { print(e); } }
数据解析
当调用Dio.get()
方法时,可以从网络获得json数据,我们可以将这些数据进行解析处理,方便UI搭建的使用。 实例如下:
//该函数从调用服务器某一个接口,获得json数据,并将解析后的Dart model返回。
Future<Course> getCourse(String studentID) async {
Dio dio = new Dio();
Response response;
response = await dio.request(
'http://www.baidu.com',//将此替换为后端服务器地址
options: Options(method: "GET", responseType: ResponseType.plain),
);
String ss = response.data;
dynamic jsonList = json.decode(ss);
Course tempStr = new Course.fromJson(jsonList);//将json解析为Dart类,
return tempStr;
}
Course.fromJson
的实现有很多种方式,可以手写或者自动生成。在此推荐一个json-Dart 转换的网页json to Dart。该网页可以自动根据给定的json数据生成Dart类。例如对于如下格式的json数据:
{
"course_name": "计算机网络",
"department": "计算机学院"
}
生成的Dart 类为:
class course {
String courseName;
String department;
course({this.courseName, this.department});
course.fromJson(Map<String, dynamic> json) {
courseName = json['course_name'];
department = json['department'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['course_name'] = this.courseName;
data['department'] = this.department;
return data;
}
}
FutureBuilder页面渲染
在实现网络请求的调用和数据解析后,我们需要把获得的数据应用到UI的渲染中。此时就要用到``FutureBuilder`。
FutureBuilder
是一个将异步操作和异步UI更新结合在一起的类,通过它我们可以将网络请求,数据库读取等的结果更新的页面上。其构造方法如下:
FutureBuilder({
Key key, Future<T> future, //表示此构建器当前连接的异步计算。
T initialData,
@required
AsyncWidgetBuilder<T> builder //是一个基于异步交互构建widget的函数,这个函数接受两个参数`BuildContext context`与 `AsyncSnapshot<T> snapshot`,并应该返回一个widget。其中异步计算获得的数据就保存在`snapshot.data`中,在builder中可以通过`snapshot.data`获得异步数据。
})
一个使用实例
//网络调用未返回数据时,返回加载动画;返回数据后展示返回数据的'course_name'值。
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('主题'),
),
body: FutureBuilder<CommonModel>(
future: getCourse(),//调用上面数据解析部分封装好的网络请求函数getCourse()为异步计算函数。
builder:
(BuildContext context, AsyncSnapshot<CommonModel> snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.none:
return new Text('Input a URL to start');
case ConnectionState.waiting:
return new Center(child: new CircularProgressIndicator());
case ConnectionState.active:
return new Text('');
case ConnectionState.done:
if (snapshot.hasError) {
return new Text(
'${snapshot.error}',
style: TextStyle(color: Colors.red),
);
} else {
return Text(${snapshot.data.courese_name}$);
}
}
}),
),
);
}
State生命周期
State是构造StatefulWidget必不可少的组件。State的生命周期主要包括三个阶段:创建、更新、销毁。
-
创建
State Widget的创建流程为:
- 构造方法,接受父组件的数据,并调用createState
- initState。在这个函数中可以对State中的状态值进行初始化。
- didChangeDependencies 。在initState后被调用,可以用来处理 State 对象依赖关系的变化。
- build。完成以上的函数后,在build方法中,将根据父 Widget 传递过来的初始化配置数据及 State 的当前状态,创建一个 Widget 然后返回。
-
更新
State Widget的状态更新可以通过三种途径:setState、didChangeDependencies 或 didUpdateWidget 触发。
- setState。当状态值发生改变的时候,可以通过调用setState方法通知flutter对Widget进行重绘(build)。
- didChangeDependencies。当State依赖的数据发生改变时,Flutter 会回调该方法,随后触发组件构建。
- didUpdateWidget。Widget 的配置发生变化时,或热重载时,系统会回调该方法。
-
销毁
当 State 被永久地从视图树中移除时,Flutter 会调用 dispose 方法,而一旦 dispose 方法被调用,组件就要被销毁了,因此可以在 dispose 方法中进行最终的资源释放、移除监听、清理环境等工作
State生命周期可以用一下图来描述:
一些组件的应用
在出现弹窗时禁用实体返回键
典型场景:在进行网络请求过程中,我们不希望用户在中途进行其他操作。此时,我们可以使用一个过渡动画来提示用户目前正在进行网络调用。
过渡动画的实现方法有很多,最常见的是进行弹窗提醒。所以我们需要维持网络请求过程中UI始终为弹窗页。即要求:
- 点击对话框以外的区域不隐藏弹窗——barrierDismissible属性
- 点击实体返回键不隐藏弹窗——在原有弹窗组件中外包裹一层WillPopScope,并设置onWillPop属性为false。
举例:
showDialog(
barrierDismissible: false,//barrierDismissible属性,控制点击对话框以外的区域是否隐藏对话框,为flase时不隐藏
context: context
builder: (context) {
return WillPopScope(//在原有的弹窗Wigdet外,嵌套WillPopScope,并设置onWillPop的返回值为false
child: Center(
Text('加载中'),
),
onWillPop: () {
return new Future.value(false);
},
);
});
点击空白处回收键盘
在与键盘有交互的页面,用户常常进行的一个操作是点击空白处回收键盘。实现这一功能的也很简单,只需要在页面的最外层包裹一层GestureDetector,并在onTap时取消键盘焦点。
GestureDetector用于用户交互,进行基本的手势控制,其基本属性如下所示。
GestureDetector({
Key key,
this.child,
this.onTapDown,//可能导致点击的指针已联系到屏幕的特定位置
this.onTapUp,//触发点的指针已停止在特定位置与屏幕联系
this.onTap,//发生了点击。
this.onTapCancel,//触发onTapDown的指针取消触发
this.onDoubleTap,//双击
this.onLongPress,//长按
this.onLongPressUp,//长按结束
this.onVerticalDragDown,//
this.onVerticalDragStart,//指针已经接触到屏幕,而且可能开始垂直移动。
this.onVerticalDragUpdate,//与屏幕接触并垂直移动的指针沿垂直方向移动
this.onVerticalDragEnd,//以前与屏幕接触并垂直移动的指针不再与屏幕接触,并且当其停止接触屏幕时以特定速度移动。
this.onVerticalDragCancel,//
this.onHorizontalDragDown,//
this.onHorizontalDragStart,//
this.onHorizontalDragUpdate,//
this.onHorizontalDragEnd,//
this.onHorizontalDragCancel,//
// onPan可以取代onVerticalDrag或者onHorizontalDrag,三者不能并存
this.onPanDown,//指针已经接触屏幕并开始移动
this.onPanStart,//与屏幕接触并移动的指针再次移动
this.onPanUpdate,//先前与屏幕接触并移动的指针不再与屏幕接触,并且当它停止接触屏幕时以特定速度移动
this.onPanEnd,//先前触发 onPanDown 的指针未完成
this.onPanCancel,//
// onScale可以取代onVerticalDrag或者onHorizontalDrag,三者不能并存,不能与onPan并存
this.onScaleStart,//
this.onScaleUpdate,//
this.onScaleEnd,//
this.behavior,
this.excludeFromSemantics = false
})
实例:
class _CourseCommentWritePage extends State<CourseCommentWritePage> {
TextEditingController commentController = new TextEditingController();
FocusNode commentNode = new FocusNode();
Widget build(BuildContext context) {
print("build_write");
return Scaffold(
appBar: AppBar(
title: Text("标题"),
),
body: GestureDetector(
onTap: () {
commentNode.unfocus();//取消键盘焦点
},
child:Container(
decoration: BoxDecoration(
border: new Border.all( 2.0, color: Colors.black12),
borderRadius: new BorderRadius.all(new Radius.circular(10.0)),
),
child: TextField(
enabled: _enable,
maxLines: 12,
focusNode: commentNode,
controller: commentController,
decoration: InputDecoration(
hintText: '请输入你对课程的评价',
border: InputBorder.none,
),
),
),
)
}
在弹出键盘时,防止页面溢出
在我们实际的项目开发中,经常会遇到页面UI内容过多,导致手机一屏展示不完的情况出现,在Flutter中我们通常使用SingleChildScrollView
处理滑动,即使用SingleChildScrollView
组件将普通组件包裹。
同样,可以使用SingleChildScrollView
中将表单TextFiled
包裹,以防止溢出键盘弹出导致的溢出。
SingleChildScrollView
组件:
const SingleChildScrollView({
Key key,//滚动方向,默认是垂直方向
this.scrollDirection = Axis.vertical,//是否按照阅读方向相反的方向滑动
this.reverse = false,//内容边距
this.padding,//是否使用widget树中默认的PrimaryScrollController
bool primary,//此属性接受一个ScrollPhysics类型的对象,它决定可以滚动如何响应用户操作,比如用户滑动完抬起手指后,继续执行动画,或者滑动到边界时,如何显示。
this.controller,
this.child,
})
在特定条件下禁用RaiseButton
RaiseButton是一个按钮组件,其一些属性作用如下:
const RaisedButton({
Key key,
@required VoidCallback onPressed,//被点击时的回调函数
ValueChanged<bool> onHighlightChanged,//水波纹高亮变化回调,按下返回true,抬起返回false
ButtonTextTheme textTheme,//按钮的主题
Color textColor,//文字的颜色
Color disabledTextColor,//按钮禁用时候文字的颜色
Color color,//按钮的背景颜色
Color disabledColor,//按钮被禁用的时候显示的颜色
Color highlightColor,//点击或者toch控件高亮的时候显示在控件上面,水波纹下面的颜色
Color splashColor,//水波纹的颜色
Brightness colorBrightness,//按钮主题高亮
double elevation,//按钮下面的阴影
double highlightElevation,//高亮时候的阴影
double disabledElevation,//按下的时候的阴影
EdgeInsetsGeometry padding,
ShapeBorder shape,//设置形状
Clip clipBehavior = Clip.none,
MaterialTapTargetSize materialTapTargetSize,
Duration animationDuration,
Widget child,
})
disabledColor
字段用于设置按钮被禁用时显示的颜色。但是可以发现其实并没有将按钮设为禁用的字段。查找资料发现,当onpress()的返回值为null时,按钮自动被设为禁用。
例如:
class MyPage extennds StatelessWidget(){
bool _enable = true;
Widget build(BuildContext context){
return RaisedButton(
child: Text("修改"),
color: Colors.lightBlue,
disabledColor: Colors.grey,
onPressed:
_enable ? () => setTextEnable() :null,//当_enable为false时,禁用按钮
),
}
void setTextEnable() {
setState(() {
_enable = !_enable;
});
}
}
//值得注意的是,不能将onpress的回调函数写为以下形式,写成这样并不能在当_enable为false时,禁用按钮
onpress:() {
if(_enable){
setTextEnable()
}else{
return null;
}
}