Flutter 入门与实战(二十五):使用 Post 请求增加动态

1,754 阅读5分钟

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

前言

之前几篇分别介绍了利用 Dio 完成后台数据的获取、删除和编辑,相关文章如下:

本篇介绍如何使用 Post 请求创建动态数据,本篇相关知识点如下:

  • 导航栏右侧的 actions
  • 路由匹配先后顺序,优先匹配先定义的路由
  • 添加与编辑的异同
  • post 请求
  • 防重点击

导航栏增加操作按钮

在导航栏右上角增加操作按钮是十分常见的情况,Flutter 的 AppBar 组件提供了actions 参数,用于设置右上角的操作按钮,actions 是一个 List<Widget>,意味着可以添加多个组件。本例增加了一个图标按钮,用于进入添加动态页面:

appBar: AppBar(
    title: Text('动态', style: Theme.of(context).textTheme.headline4),
    actions: [
      IconButton(
          icon: Icon(Icons.add),
          onPressed: () {
            RouterManager.router
                .navigateTo(context, RouterManager.dynamicAddPath);
          })
    ],
    brightness: Brightness.dark,
  ),
    
  //...

RouterManager.dynamicAddPath是添加页面的路由路径常量,为 /dynamics/add。但是,我们会发现这个路由规则和/dynamics/:id ,即动态详情的实际是可以匹配的,一不小心就跳到了详情页而不是添加页面,这个时候该怎么处理呢?

Fluro路由匹配的先后次序

Fluro 的路由匹配次序是按照定义路由的先后次序进行匹配的,因此需要把更具体的路由放置在范围匹配的前面,即定义添加页面路由时要放置在详情路由的前面。这点实际上和 React Router类似,匹配到了就跳出规则,不再往下匹配。因此,在使用 Fluro 的时候需要注意定义路由的次序,否则可能会导致路由跳转不正确。

添加页面

添加页面的表单和编辑页面一样,只是没有从后台读取数据填充表单内容。我们先直接复制之前的 dynamic_edit.dart 文件,并重命名为 dynamic_add.dart,同时将DynamicEdit 替换为 DynamicAdd。与编辑页面的不同之处在于:

  1. _formData 需要提前定义,如下所示。
Map<String, Map<String, Object>> _formData = {
  'title': {
    'value': '',
    'controller': TextEditingController(),
    'obsecure': false,
  },
  'content': {
    'value': '',
    'controller': TextEditingController(),
    'obsecure': false,
  },
  'imageUrl': {
    'value': '',
    'controller': TextEditingController(),
    'obsecure': false,
  },
};
  1. _getFormWidgets构建表单组件时无需返回加载提示,直接返回表单即可。
  2. 网络请求修改为 Post 请求。
_handleSubmit() async {
  if ((_formData['title']['value'] as String).trim() == '') {
    Dialogs.showInfo(this.context, '标题不能为空');
    return;
  }

  if ((_formData['content']['value'] as String).trim() == '') {
    Dialogs.showInfo(this.context, '内容不能为空');
    return;
  }

  if ((_formData['imageUrl']['value'] as String).trim() == '') {
    Dialogs.showInfo(this.context, '图片链接不能为空');
    return;
  }

  try {
    Map<String, String> newFormData = {};
    _formData.forEach((key, value) {
      newFormData[key] = value['value'];
    });
    var response = await DynamicService.post(newFormData);
    if (response.statusCode == 200) {
      Dialogs.showInfo(context, '添加成功');
    } else {
      Dialogs.showInfo(this.context, response.statusMessage);
    }
  } on DioError catch (e) {
    Dialogs.showInfo(this.context, e.message);
  } catch (e) {
    Dialogs.showInfo(this.context, e.toString());
  }
}

我们会发现有很多方法是类似的,比如表单、表单校验以及编辑时表单数据处理。因此这些共同的地方可以进行封装,但是需要考虑实际业务添加和编辑的表单内容可能不同,比如某些字段不允许编辑等,因此考虑共通性,我们做更通用的处理,提取一个 dynamic_form.dart 类,将通用的部分统一封装进去,提高复用性。

class DynamicForm extends StatelessWidget {
  final Map<String, Map<String, Object>> formData;
  final Function handleTextFieldChanged;
  final ValueChanged<String> handleClear;
  final String buttonName;
  final Function handleSubmit;
  const DynamicForm(this.formData, this.handleTextFieldChanged,
      this.handleClear, this.buttonName, this.handleSubmit,
      {Key key})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      child: Column(children: _getForm(context)),
    );
  }

  List<Widget> _getForm(BuildContext context) {
    List<Widget> widgets = [];
    formData.forEach((key, formParams) {
      widgets.add(FormUtil.textField(key, formParams['value'],
          controller: formParams['controller'] ?? null,
          hintText: formParams['hintText'] ?? '',
          prefixIcon: formParams['icon'],
          onChanged: handleTextFieldChanged,
          onClear: handleClear));
    });

    widgets
        .add(ButtonUtil.primaryTextButton(buttonName, handleSubmit, context));

    return widgets;
  }
}

封装完之后,编辑和添加页面的_formData需要增加将构建 TextField 的字段补齐,而不是之前那样写死,这样更灵活。并且,代码将更为简洁,以添加页面为例。

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text('添加动态'),
      brightness: Brightness.dark,
    ),
    body: DynamicForm(
      _formData,
      _handleTextFieldChanged,
      _handleClear,
      '提交',
      _handleSubmit,
    ),
  );
}

防重提交

调试过程中发现,点击提交按钮时保存的数据会有多条。Flutter 如何防重提交? image.png

一般防重提交的处理方法一种是点击后禁用,等待网络请求结果返回后再启用按钮。另外一种方式就是增加Loading蒙层,在网络请求没结束前使用蒙层将页面遮挡,从而避免操作表单及按钮。这里我们采用第二种方式,通过蒙层的方式指示可以避免操作表单,也能够给出加载指示。

在 pub 上提供了一个 flutter_easyloading 的插件,可以满足这要求。具体使用是在main.dart的 MatertialApp 的 builder 参数传递EasyLoading.init()方法,初始化一个全局的EasyLoading对象,之后就可以在页面中随时调用了。显示的时候调用 showXXX 方法,消失的时候调用 dismiss 方法,可以设置多种 loading 样式,也支持自定义 loading 组件以及自定义参数,具体可以参考:flutter_easyloading。我们在提交前显示EasyLoading,接收到数据后移除EasyLoading即可。

_handleSubmit() async {
  //...校验代码
	EasyLoading.showInfo('请稍候...', maskType: EasyLoadingMaskType.black);
  //...网络请求代码
  EasyLoading.dismiss();
}

总结

本篇介绍了新增数据页面的示例,同时对于编辑和添加的页面重复部分通过封装共用的表单组件简化了页面结构和提高复用性。考虑实际操作的重复点击,还引入了 flutter_easyloading 来实现加载蒙层的效果。源码已提交至:网络章节源码。注意运行时拉取最新的后台代码运行,以免找不到后台服务加载不了数据。