Flutter开发一款物流APP始末

850 阅读3分钟

image.png

技术勇•无止尽

背景

项目本身就存在,只是之前是以PC端 + 微信小程序承载。对于客户、用户来说使用不友好,针对物流追踪、信息缓存、蓝牙打印等功能在小程序上实现效果并不好,APP的开发应用而生。

调研

其实调研因为结合自身开发团队情况,只调研了uni-app、flutter,所谓的跨平台解决方案。uni-app查看了安卓端的Demo就放弃了,切页面时有明显的链接跳转痕迹,像Html页面的切换。可能研究不深刻,初次印象比较差。Flutter的选择也是反复测试了下,并没有发现明显的交互问题。当然网络上对Flutter的风评也比较低,万物应皆原生的意味,最终都会回归原生。当然对于当下的我,Flutter在控本增效方面是优先选项。

环境搭建

因为我前端出身的缘故,采用了vscode + 夜神进行开发。首先执行flutter doctor -v详细诊断一下,主要错误是没装Visual Studio - develop for Windows,可能官方更推荐用Visual Studio工具进行开发。

Flutter 2.10.5 • channel stable
ToolsDart 2.16.2
// 模拟设备Mate 20 Pro (mobile) • 127.0.0.1:62001 • android-x86    • Android 7.1.2 (API 25)
// 真机设备 - 数据线直接连接真机,手机打开开发者模式的USB调试,USB的连接方式改为‘文件传输’
// 真机调试主要是开发设备定位、蓝牙打印等针对手机原始功能基础上开发的功能

image.png

依赖库

都在站在前人肩上摸石头过河,Flutter的生态圈一直不温不火,网上资料相对比较少。当然少则少,日常开发基本满足。

environment:
  sdk: '>=2.15.1 <3.0.0'

dependencies:
  flutter:
    sdk: flutter

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.2
  iconfont_dart: ^0.3.3
  color_dart: ^0.2.1
  flutter_swiper: ^1.1.6
  provider: ^6.0.4
  pull_to_refresh: ^2.0.0
  dio: ^4.0.6
  fluttertoast: ^7.1.6
  shared_preferences: ^2.0.15
  device_info_plus: ^2.2.0
  fluro: ^2.0.3
  oktoast: ^3.1.5
  url_launcher: ^6.1.7
  sp_util: ^1.0.2
  keyboard_actions: ^3.3.1+1
  qr_code_scanner: ^0.7.0
  common_utils: ^2.1.0
  rxdart: ^0.27.1
  cached_network_image: ^3.2.0
  bluetooth_print: ^3.0.1
  json_annotation: ^4.6.0
  flutter_easyrefresh: ^2.2.1
  sticky_headers: ^0.2.0
  city_pickers: ^1.1.0
  flutter_my_picker: ^1.0.3
  # 高德2D地图插件(支持Webhttps://github.com/simplezhli/flutter_2d_amap
  flutter_2d_amap:
    git:
      url: 'https://github.com/simplezhli/flutter_2d_amap.git'
  encrypt: ^2.0.0
  web_socket_channel: any
  hprt_print: ^3.0.2

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^1.10.11
  json_serializable: ^6.3.1

开发

瞎瞅了两天别人的项目源码,就直接开干了。

  1. 主要熟悉原生控件,Scaffold、InkWell、Container、Column、Row、Expanded、Text等,类比理解
  2. 熟悉Flutter页面的生命周期initState、build、dispose
  3. 熟悉路由传参、页面之间通讯
  4. 封装顺自己意的自定义控件
  5. 重要的一点是构建自己的顺手样式类

备注:追求的是开发体验的快乐,顺手顺意。

列表页面
  • 重写build方法,返回一个控件Widget
  • 循环渲染每一个列表item,构建列表主体,支持下拉刷新,上拉分页。
  • 支持接口请求时列表loading,无数据时主体填充。
  • 布局上将界面分割成上下两部分,上面为列表主体部分,下面为操作按钮‘创建批次’。
  • 使用封装的NavigatorUtils.push(context, PortalRouter.batchFormPage)进行路由页面跳转。
  • 使用封装的PermissionUtil.hasPermission('cmpy_batch_add')进行按钮权限控制。
  • 使用控件CustomScrollView作为列表上下滚动容器。
  @override
  Widget build(BuildContext context) {
    super.build(context);

    final Widget bottomMenu = Container(
      height: 60.0,
      padding: const EdgeInsets.symmetric(horizontal: 16.0),
      child: Theme(
        data: Theme.of(context).copyWith(
          buttonTheme: const ButtonThemeData(
            height: 44.0,
          ),
        ),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: <Widget>[
            Visibility(
                visible: PermissionUtil.hasPermission('cmpy_batch_add'),
                child: Expanded(
                    child: MyButton(
                        backgroundColor: Colours.app_main,
                        textColor: Colours.text_white,
                        text: '创建批次',
                        minHeight: 40,
                        minWidth: 160,
                        onPressed: () {
                          NavigatorUtils.push(context, PortalRouter.batchFormPage);
                        }))),
          ],
        ),
      ),
    );
    return Column(children: [
      Expanded(
          child: NotificationListener(
        onNotification: (ScrollNotification note) {
          if (note.metrics.pixels == note.metrics.maxScrollExtent) {
            _loadMore();
          }
          return true;
        },
        child: RefreshIndicator(
          onRefresh: _onRefresh,
          displacement: 64.0, //40 + 24
          /// 默认40, 多添加的80为Header高度
          child: Consumer<BatchPageProvider>(
            builder: (_, provider, child) {
              return CustomScrollView(
                /// 这里指定controller可以与外层NestedScrollView的滚动分离,避免一处滑动,5个Tab中的列表同步滑动。
                /// 这种方法的缺点是会重新layout列表
                controller: _tabIndex != provider.tabIndex ? _controller : null,
                key: PageStorageKey<String>('$_tabIndex'),
                slivers: <Widget>[
                  // SliverOverlapInjector(
                  //   ///SliverAppBar的expandedHeight高度,避免重叠
                  //   handle:
                  //       NestedScrollView.sliverOverlapAbsorberHandleFor(context),
                  // ),
                  child!,
                ],
              );
            },
            child: SliverPadding(padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), sliver: _listItems(context)),
          ),
        ),
      )),
      bottomMenu
    ]);
  }

4d61e42a6d305319e348cb0f7aa0b27.png

遇到的问题

  • 空安全问题,对于常规的编码风格要求会更加严格。让你真正对每个变量、参数做出精准的判断处理。
  • 布局控件搭配使用问题,会遇见各种奇奇怪怪的布局控件使用不兼容问题。仔细查看错误日志,认真分析,勇于思考,百度不出来。
  • 关注各个手机屏幕下文本溢出、布局失效等问题。手动处理文本换行、布局修正。
  • 对照接口文档手动编写model类,比较费时间,时刻关注空安全。
  • 依赖库中各个依赖包的兼容问题,慎重升级,否则可能搞一下午。
  • 慎重升级开发工具,否则莫名报错,痛苦解决。

后记

开发过程经历过偶尔的痛苦阶段,还是很丝滑。Dart语法介于JavaScriptJava之间,页面生命周期类React(不过比React清晰很多)。APP性能方面并没有网传的交互卡顿,体验差等问题。