ZHANGYU.dev

October 14, 2023

Flutter初次使用的一些总结

Flutter7.6 min to read

项目结构

项目结构png

因为我是前端,所以按照平时写前端对习惯分目录了

项目的依赖

网络请求

    dio.interceptors.add(
      InterceptorsWrapper(
        onRequest: (RequestOptions options) async {
          // 判断网络状态
          ConnectivityResult connectivityResult =
              await (Connectivity().checkConnectivity());
          if (connectivityResult == ConnectivityResult.none) {
            // 没网返回网络错误信息
            return dio.resolve(errorRes);
          }
          // 加上token
          final String token = Global.token;
          if (token != null) options.headers["Authorization"] = "bearer $token";
          return options;
        },
        onResponse: (Response response) {
          // token失效对判断
          if (response.data["code"] == 401) {
            print("token无效");
            showToast("登录状态过期,请重新登录");
            Global.clear();
            Global.navigatorKey.currentState.pushReplacementNamed("login");
          }
          return response;
        },
        // 后台报错了要返回一组错误信息
        onError: (e) => dio.resolve(requestErrorRes),
      ),
    );

dio 的拦截器,统一带上token,并且对网络状态做判断,错误也返回数据防止页面不能操作了

这里的Global 是我封装的一个静态类,具体可以参考这个教程

main 函数里,先于runApp函数调用Global类的初始化方法,初始化shared_preferences以及极光推送,并且传出一个GlobalKey,赋值给MJaterialAppnavigatorKey,这样做可以让全局只需要调用Global静态类,就可以直接操作navigator,比如上图在请求中对token失效后对跳转。执行完Global对初始化后然后再执行runAppmain函数如下

void main() {
  // 先初始化
  Global.init().then((GlobalKey<NavigatorState> navigatorKey) {
    runApp(
      OKToast(
        radius: 6,
        child: App(navigatorKey: navigatorKey),
      ),
    );
    // 透明状态栏
    if (Platform.isAndroid) {
      SystemUiOverlayStyle systemUiOverlayStyle =
          SystemUiOverlayStyle(statusBarColor: Colors.transparent);
      SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle);
    }
  });
}

屏幕适配问题

查阅了一些方法,感觉都太复杂了,还是参考前端的rem布局方式,写了一个计算的函数,所有数值都用这个函数,好像没啥问题,不过公司都 ios 开发说这种等比方式很恶心 orz

double setSize(double width) {
  final double screenWidth = MediaQueryData.fromWindow(window).size.width;
  final double percent = screenWidth / 375;
  return percent * width;
}

效果和遇见的一些问题

可以拖动收掉的菜单

抽屉效果gif

这个效果,简单看了看官方的示例项目,就决定是showModalBottomSheet了,不过很快就遇见了问题,使用showModalBottomSheet创建的页面,高度只能是屏幕的一半,网上看了看有些就得魔改源码,发现一个参数isScrollControlled可以让页面全屏,于是我就把背景设为透明,child设一个定高,就可以上面留白了

    showModalBottomSheet(
        // 页面可以全屏
        isScrollControlled: true,
        // 背景透明
        backgroundColor: Colors.transparent,
        context: context,
        builder: (BuildContext context) {
            // 上面空白,背景色,圆角
            return Container(
              height: MediaQuery.of(context).size.height - setSize(64),
              decoration: BoxDecoration(
                color: Colors.white,
                borderRadius: BorderRadius.only(
                    topLeft: Radius.circular(24),
                    topRight: Radius.circular(24),
                    ),
              ),
          );
        }
    )

写好样式后,做数据交互的时候又出问题了,就算调用了setState,这个滑动页面里也不会变,得拖一下才能变,原因是因为其实这个能拖拽的滑动页面,也是一个新的页面了,调用之前页面的setState当然这个页面不会变了

有一个叫StatefulBuilderWidget,可以创建一个state,给一个setState的方法,用这个包装一下里层的Widget,就可以让这个页面改变了

 showModalBottomSheet(
        ...
        builder: (BuildContext context) {
          // 创建state修改弹窗页面
          return StatefulBuilder(builder:
              (BuildContext statefulContext, StateSetter setModalState) {
                return Container(
                    child: InactiveBrokerItem(
                        onTap: () {
                          setModalState(() {
                            _activatedList.add(brokerItem.phone);
                          });
                        }
                    )
                );
            }
        );
        ...
    }
)

不过这个终究还有有一个问题,就是需要调用参数StateSetter这个方法来改变页面,所以里面的方法都必须得写成匿名函数直接放Widget上,不能放外面了

透明的页面

分享页.jpeg

这个页面是一个分享页,但是是模糊透明的,如果是用命名路由,好像不能做到这样的效果,需要用到PageRouteBuilder 代码如下

Navigator.of(context).push(
      PageRouteBuilder(
        transitionDuration: Duration(milliseconds: 200),
        // 这个参数就是控制是否透明
        opaque: false,
        barrierDismissible: true,
        pageBuilder: (BuildContext context, Animation animation,
                Animation secondaryAnimation) =>
            SlideTransition(
          position: Tween<Offset>(begin: Offset(0, 1), end: Offset.zero)
              .animate(animation),
                // 里面的页面也需要设置一个透明的背景
          child: Scaffold(
                     backgroundColor: Colors.transparent,
                     body:...
                 ),
        ),
      ),
    );

全面屏的适配

一半来说Scaffold已经自动设置好了,如果还有问题的页面可以在外面套一个SafeArea,但是这样如果是自定义的bottomSheet会出现下面的问题 bottomSheet.png

因为SafeArea的原因,下面没有背景色,解决办法是把这一块用Stack套起来,定位一个高度相同的白色Container到最下方,在不是全面屏的手机上白色背景会被内容盖住,全面屏上会下移变成底

最后效果

bottomSheet效果.png

相机页面

这个我觉得是我写的最难的页面,大概研究了 10 多个小时……

最后效果如下

相机.gif

因为用的camera插件,在我的安卓机上,屏幕太长了,相机区域不能满屏,而在 iphone 上又是满屏,在 iphone x 上又不是满屏,样式不能通用,搞的我很头大,这个插件相机的区域好像是按比例来的,插件能得到一个相机区域宽高比值,用屏幕大小和比值计算,得出相机高度,再用屏幕高度 - 相机高度,得到操作栏的高度,如果相机的高度已经是全屏了,操作栏就不显示白色背景栏,覆盖在相机区域上,就适配普通 iphone

我之前在拍摄的同时就压缩图片转为 base64,在我的安卓机上一点问题都没有,在 iphone 上直接卡死机,后来就只有把拍摄和压缩的逻辑分开了

图片裁剪

上传头像这样的功能,可能需要用户自己选择区域,直接用image_cropper这个插件,非常好用

密码设置页面

密码设置.gif

比较难写的就是这种输入框,不过好在 pub 上有这类插件,我用的是pin_code_text_field,每次输入满会收起键盘触发事件,不过这个插件样式不能自定义,只能 copy 下来魔改一番样式

页面的滚动是用PageView做的,遇见的一个问题就是焦点问题,最后在PageView的事件里进行了判断,看是应该哪一个输入框聚焦

    _pageController = PageController()
      ..addListener(() {
        if (_pageController.page.toInt() == 1)
          FocusScope.of(context).requestFocus(_passwordFocusNode);
        else
          _passwordFocusNode.unfocus();
        if (_pageController.page.toInt() == 2)
          FocusScope.of(context).requestFocus(_verificationFocusNode);
        else
          _verificationFocusNode.unfocus();
      });

还有一个问题就是,如果密码输入错误,回到第一个密码输入页面会报focusNode到错误,我这里每一个密码输入页面都是一个StatefulWidgetPageView在页面滚出去以后会销毁,需要让这个页面 mixin AutomaticKeepAliveClientMixin来保持才行

推送问题

在 ios 上很正常,在安卓端能收到推送但是不弹出通知,我也不懂原生,好在公司也不咋个重视安卓,这个功能也就暂时算了……

还有些错误

flutter_image_compressimage_picker在安卓上都会报错,需要修改android/build.gradleclasspath 'com.android.tools.build:gradle:3.2.1'3.3.2

全局事件

这个项目不太复杂,大多数是功能,需要保存操作展示数据的地方很少,所以就直接使用的event_bus,还是挺好使的,其实我有研究过fish_redux,不过嘛没有官方文档,示例看半天没搞懂……,时间比较急,所以就算了

最后的总结

其实公司有安卓和 ios 开发,不过这个 app 只是一些比较简单的功能,而且也是内部使用,在总监的支持下,开始了 flutter 的第一次尝试,从开始学,边学边做,到最后完成初版,总共 20 多个页面,花了 20 天左右,可以看出 flutter 还是比较容易入门的,入门容易,但是要学好还是很难的,需要花很多时间

用下来感觉,flutter 画这些页面真是非常好使,布局多少和前端沾点边,写页面也很舒适轻松,而且官方提供了很多Widget,包括一些动画,简单又好使,用户体验还是很不错,效果上很接近原生了,最后 build 下来,就算在 iphone 5 上,也还算流畅,旗舰机型就不说了,丝滑

不过,没有原生开发的经验,一旦和原生交互起来,就很难搞了,就比如推送,搞得头大,包括 ios 的开发者账号,还有推送证书配置,作为一个小小前端真的很懵逼……而且,我觉得 ios 端相比下开发还简单点,不需要配置太多,安卓端得配置一小堆东西

我还是初学者,不懂优化,但是 flutter 还是很流畅,ios 开发的端,xcode 看占用最多也就 100mb 内存,我这个端进去就 100mb,拍照直接飙 500mb,在优化这方面还是有很深的学问,需要升入学习

flutter 现在确实有一些问题,有些问题我看 github 上 17 年就有了,也没人解决,比如TabBar 文字跳动,堆了 7000 多个 issue,dart 这个语言也有 5000 个,相比之下 react native 才几百个,路漫漫其修远啊,个人还是很看好 flutter 都,希望以后可以大放异彩吧

dart 语言得学习,也让我感受到强类型都好处,写着真的很舒服,js 那一堆莫名的联想哈哈,所以学好 ts 还是至关重要的,以后得多多使用 ts

上个月买了台 mbp15,真是卡出新高度啊,就这篇文章,就几个 gif,就卡不行,编译器不好使吗?有道云里同样也卡,而 windows 下一点不卡,不过 mac 系统还是挺好用的,完全没能让我感受到 6 核 12 线程到强劲,如果是 windows 这个价钱就是 3700x+2080super 吧,相比之下真是 low 的淌口水……

研究了下 pm2 自动部署部署,居然服务器内存不够,部署不了,我也是醉了,只能又 ssh 传了,总觉得傻傻的