浅尝Flutter(一)

背景

公司新项目,公司想要更高效的人力开发方式,之前的app采用MVVM+Kotlin的android和Swift开发IOS原生单独开发,优势和劣势都很明显,开发效率低,但是apk的体验和大小都比较极致,起码我负责的android是同行业竞品里包体积最小的,甚至比平均水平小50%,这次B端项目,这种极致的体验要求不高,反而是效率更重要。

通过对比RN,Uniapp,Flutter方式的优劣势,以及团队人员的组成以移动端为主情况,最后我的决策是使用Flutter进行开发。

使用flutter的好处主要是,主要是移动端上手更快,本身是一个偏UI引擎的开发语言,而且本身有正规军google维护,所以对生态还是比较放心。

开始

  • 开发工具支持:VScode,Android studio
  • 环境要求:Xcode , android SDK, Flutter SDK
    一开始也不清楚,直接使用的Flutter doctor去检测的环境,实际上Xcode和android SDK 应该是可以2选一的,我因为是Android开发,后面因为磁盘空间不够,选择把Xcode卸载了,实际开发中基本没用过,因为我们IOS打包有IOS开发负责,如果你需要打包IOS,Xcode还是需要的。

1、框架

实际这个过程也是从0到1,没有找参考项目。
但是框架搭建都大差不差,都是从一些基础服务和基础能力开始。
基础服务提供埋点,推送,网络请求,图片加载,数据库,文件存储,sp存储等等,
基础UI能力下拉刷新,页面的loading和重试框架,基类,通用弹窗等。
跨端信息获取,统一的theme,页面跳转路由等。

因为是多人协作边开发边学习,首先约定好文件和变量命名规则,然后项目搭建前面1-2天由我开始把最迫切的基础能力网络请求,图片加载,文件,sp存储搭建好,并且编写入口启动页和登录页。
然后把各类基础能力采用组合的方式把能力封装出去,base_stateful和base_state_less。

对于Flutter这种所有的页面和view全部集成自Widget的然后在分成2个stateful和stateless控件的设计方式,属实适应了一会儿,这让我想起了android的Compose和单项数据流架构的MVI

实际在使用Flutter之前我也没用过Compose,我们用Flutter写这个项目的时候实际KMM还没有成熟稳定版本,现在已经有了稳定版本,接下来我可能会接触下KMM。

对于基础能力各家架构各不相同,我采用了组合的方式把主要的能力插拔式的采用组合的方式集成在基类中。

abstract class BaseState<T extends BaseStatefulWidget> extends State<T> {
  StreamController<PageData> pageStatus = StreamController<PageData>();

  //页面加载失败显示
  Widget? error;

  //页面加载
  Widget? loading;

  //空页面
  Widget? empty;

  //内容
  PagerController? pageController;

  @override
  void initState() {
    super.initState();
  }

  @override
  void setState(VoidCallback fn) {
    if (mounted) {
      super.setState(fn);
    } else {
      print("Widget unmount. skip setState");
    }
  }

  DialogLoadingController? _dialogLoadingController;

  ///在页面上方显示一个 loading widget
  ///共有两种方法,showProgressDialog是其中一种
  ///具体参见 : loading_progress_widget.dart
  ///如果需要在 [dismissProgressDialog] 方法后跳转到其他页面或者执行什么
  ///使用 参数 [afterDismiss] 和 [dismissProgressDialog.afterPopTask] 不建议同时用使用。
  ///[loadingTimeOut] 超时时间,单位秒。
  /// * 抵达这个时间后,将自动关闭loading widget,默认15秒
  showProgressDialog(
      {Widget? progress,
      Color? bgColor,
      int loadingTimeOut = 15,
      AfterLoadingCallback? afterDismiss}) {
    if (_dialogLoadingController == null) {
      _dialogLoadingController = DialogLoadingController();
      Navigator.of(context)
          .push(PageRouteBuilder(
              settings: const RouteSettings(name: loadingLayerRouteName),
              ///使用默认值效果可能不好
              transitionDuration: const Duration(milliseconds: 0),
              reverseTransitionDuration: const Duration(milliseconds: 0),
              opaque: false,
              pageBuilder: (ctx, animation, secondAnimation) {
                return LoadingProgressState(
                        controller: _dialogLoadingController,
                        progress: progress,
                        bgColor: bgColor,
                        loadingTimeOut: loadingTimeOut)
                    .transformToWidget();
              }))
          .then((value) {
        _dialogLoadingController?.invokeAfterPopTask(value);
        _dialogLoadingController = null;
        if (afterDismiss != null) {
          afterDismiss(value);
        }
      });
    }
  }

  ///隐藏loading
  /// * 注意: [afterPopTask] 会在loading隐藏后被调用,但是如果用户主动取消的话,将不会被调用
  /// *       用户主动取消的将会调用[showLoading]的[afterDismiss]
  void dismissProgressDialog({AfterLoadingCallback? afterPopTask}) {
    if (afterPopTask != null) {
      _dialogLoadingController?.holdAfterPopTask(task: afterPopTask);
    }
    _dialogLoadingController?.dismissDialog();
  }

  @override
  void dispose() {
    _dialogLoadingController = null;
    pageController?.dispose();
    pageController = null;
    super.dispose();
  }

  //把任务抛到gpu绘制的下一帧执行
  void postAtNextFrame(Function task) {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      task.call();
    });
  }
}

其中page_controller中实现了页面的基础状态和UI逻辑,并且实现基类的页面可以自定义空页面和loading等。

而pageStatus表示页面的状态流,page_controller会负责监听这个状态流进行页面状态UI的实时更新。

网络请求采用了通用的Dio库并进行了基础封装。

extension NetFunc on BaseState {
  ///get请求
  ///[showLoadingAuto] 是否自动显示和隐藏loading ,推荐在页面加载完成之后的其他网络请求中使用
  ///[toast] 是否自动弹出失败请求toast
  ///
  ///[antiContentFlow]只会走数据加载的状态流
  ///主要应用场景,在于页面已经加载出来之后,如果继续走flow会走完整的
  ///[loading -> content | error | empty 流]
  ///某些场景不需要loading
  ///
  ///[enableFlow]走完整的数据流loading , content , empty  failure
  Future<dynamic> get(String path, Map<String, dynamic> params,
      {Function? successCallBack,
      Function? errorCallBack,
      bool showLoadingAuto = false,
      bool enableFlow = true,
      bool antiContentFlow = false,
      bool toast = true,
      bool refresh = false,
      CancelToken? cancelToken}) async {
    if (showLoadingAuto) showProgressDialog();

    //模拟弱网
    //await Future<void>.delayed(Duration(seconds: 2));
    cancelToken?.cancel();

    if (enableFlow) pageStatus.add(PageData(PageStatus.LOADING));

    int currentTime = DateTime.now().millisecondsSinceEpoch;

    await NetManager.getInstance().get(path, params, (data) {
      if (showLoadingAuto) {
        dismissProgressDialog(afterPopTask: (e) {
          try {
            successCallBack?.call(data);
          } on Exception catch (error) {
            //做了一些日志处理
          }
        });
      } else {
        try {
          successCallBack?.call(data);
        } on Exception catch (error) {
          //做了一些日志处理
        }
      }

      if (enableFlow || antiContentFlow) {
        if (pageController?.emptyPrediction?.call(data) == true) {
          pageStatus.add(PageData(PageStatus.EMPTY));
        } else {
          // pageStatus.add(PageData(PageStatus.ERROR));
          pageStatus.add(PageData(PageStatus.CONTENT, data: data));
        }
      }
    }, (BaseBeanEntity error) {
      if (enableFlow) {
        pageStatus.add(PageData(error.code != ResultCode.NO_NETWORK
            ? PageStatus.ERROR
            : PageStatus.ERROR_NO_NET));
      }

      if (toast) {
        ToastUtils.showToast(
            context: context, msg: "Net Error Code:${error.code},${error.msg}");
      }
      if (showLoadingAuto) dismissProgressDialog();

      errorCallBack?.call(error);
    },startRequestTime : currentTime);
  }

  ///post请求
  ///[showLoadingAuto] 是否自动显示和隐藏loading
  ///[toast] 是否自动弹出失败请求toast
  ///
  ///[antiContentFlow]只会走数据加载的状态流
  ///主要应用场景,在于页面已经加载出来之后,如果继续走flow会走完整的
  ///[loading -> content | error | empty 流]
  ///某些场景不需要loading
  ///
  ///[enableFlow]走完整的数据流loading , content , empty  failure
  Future<dynamic> post(String path, Map<String, dynamic> params,
      {Function? successCallBack,
      Function? errorCallBack,
      bool showLoadingAuto = false,
      bool enableFlow = true,
      bool antiContentFlow = false,
      bool toast = true,
      CancelToken? cancelToken}) async {
    if (showLoadingAuto) showProgressDialog();
    cancelToken?.cancel();

    //模拟弱网
    // await Future.delayed(const Duration(seconds: 3));

    if (enableFlow) pageStatus.add(PageData(PageStatus.LOADING));

    int currentTime = DateTime.now().millisecondsSinceEpoch;

    NetManager.getInstance().post(path, params, (data) async {
      if (showLoadingAuto) {
        dismissProgressDialog(afterPopTask: (e) {
          try {
            successCallBack?.call(data);
          } on Exception catch (error) {
            //做了一些日志处理
          }
        });
      } else {
        try {
          successCallBack?.call(data);
        } on Exception catch (error) {
           //做了一些日志处理
        }
      }

      if (enableFlow || antiContentFlow) {
        if (pageController?.emptyPrediction?.call(data) == true) {
          pageStatus.add(PageData(PageStatus.EMPTY));
        } else {
          pageStatus.add(PageData(PageStatus.CONTENT, data: data));
        }
      }

    }, (error) {
      if (enableFlow) {
        pageStatus.add(PageData(error.code != ResultCode.NO_NETWORK
            ? PageStatus.ERROR
            : PageStatus.ERROR_NO_NET));
      }

      if (toast) {
        ToastUtils.showToast(
            context: context, msg: "Net Error Code:${error.code},${error.msg}");
      }
      if (showLoadingAuto) dismissProgressDialog();

      errorCallBack?.call(error);
    },startRequestTime : currentTime);
  }
}

实际这个网络扩展类扩展了base基类的能力,它主要的作用是根据网络请求对UI流进行分发。底层的请求代码就不贴了,非核心。

实际上页面的状态比这里定义的页面状态,更复杂。
比如:

  • 页面页面中多请求,谁该主导loading状态还是,说要等所有请求完再显示Content。
  • 页面加载完之后,页面条件变化,需要重新请求部分内容,这个时候覆盖页面的loading和蒙层的loading谁该显示。
  • 还有Flutter的单项数据流,在setState中执行的逻辑不能继续触发setState等。
  • 还有复杂的路由跳转维护,电商业务的创建订单结束复杂的页面跳转分发逻辑需要强大的路由基类支持等。

篇幅太长下章继续 To be continue....

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容