【技术博客】Flutter—使用网络请求的页面搭建流程、State生命周期、一些组件的应用
阅读原文时间:2023年07月10日阅读:1

使用网络请求的页面搭建流程

​ 在开发APP时,我们常常会遇到如下场景:进入一个页面后,要先进行网络调用,然后使用调用返回的数据进行页面渲染。

​ 这种页面搭建流程大致为:调用网络请求,获得json格式的数据—解析获得的数据为Dart类 — 将Dart数据传回UI。在返回数据前,可以在页面先放置一个加载动画;获得数据后,使用数据进行进行页面重绘。

网络请求

​ Flutter的网络请求常常使用的库有httpdio,其中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的创建流程为:

  • 更新

    State Widget的状态更新可以通过三种途径:setState、didChangeDependencies 或 didUpdateWidget 触发。

    • setState。当状态值发生改变的时候,可以通过调用setState方法通知flutter对Widget进行重绘(build)。
    • didChangeDependencies。当State依赖的数据发生改变时,Flutter 会回调该方法,随后触发组件构建。
    • didUpdateWidget。Widget 的配置发生变化时,或热重载时,系统会回调该方法。
  • 销毁

    当 State 被永久地从视图树中移除时,Flutter 会调用 dispose 方法,而一旦 dispose 方法被调用,组件就要被销毁了,因此可以在 dispose 方法中进行最终的资源释放、移除监听、清理环境等工作

State生命周期可以用一下图来描述:

一些组件的应用

在出现弹窗时禁用实体返回键

​ 典型场景:在进行网络请求过程中,我们不希望用户在中途进行其他操作。此时,我们可以使用一个过渡动画来提示用户目前正在进行网络调用。

​ 过渡动画的实现方法有很多,最常见的是进行弹窗提醒。所以我们需要维持网络请求过程中UI始终为弹窗页。即要求:

  1. 点击对话框以外的区域不隐藏弹窗——barrierDismissible属性
  2. 点击实体返回键不隐藏弹窗——在原有弹窗组件中外包裹一层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(width: 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;
    }
}

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器