前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >用 Flutter 重构你的应用

用 Flutter 重构你的应用

作者头像
用户1097444
发布2022-06-29 14:41:41
6570
发布2022-06-29 14:41:41
举报
文章被收录于专栏:腾讯IMWeb前端团队

导语:腾讯在线教育团队(简称:OED)已经将 Flutter 在 『腾讯企鹅辅导』的产品中落地了,IMWeb团队也积极参与,共同推进产品落地和技术的提升。本文描述了最近基于 Flutter 模拟开发企鹅辅导 APP 的实践经历,从 0 到 1 的进行了样板工程的落地实践,希望可以让您近距离的了解和感受 Flutter 开发的过程。

前言

    恰逢十一放假,年假多到用不完,索性也就多请了几天,回了一趟我大东北,处理一些个人私事。切实的感受到了北方的雨 + 雪 ,以及真实的冷!在假期的时候,就萌生了一个想法,趁着有整块的时间,可以仿照 企鹅辅导App 写一个 Flutter 的实例工程。OED 的客户端团队已经用 Flutter 做了一个 iPad 版本, 因此我也想独立尝试一下,正如之前的文章当 Flutter 遇见 Web,会有怎样的秘密 中提到的,光说不练假把式,实践方可出真知。因此,必须自己动手,系统的尝试一下。同时自己也希望迅速的求证和落地一个项目,看看在这个领域内有怎样的机会。因此用了几天时间,怀着忐忑的心情,做了一只小白鼠,进行了辅导实例样板工程的开发体验,不用处理其他事情,可以长时间专注写代码,确实也是被爽到了!

    样板工程的目的就是 熟悉 Flutter 的开发流程,关注如何调试、UI 布局、事件,以及基础能力的使用。最开始做的时候,很多东西也都没了解清楚,一边做一边摸索,毕竟只有真实体验之后,才能发现开发痛点,为后续的工程化和动态运营做一些技术上的认知和理解。

01 实践案例

【左侧 改版前 RN】 、【中间 Flutter Demo】、【右侧 To Web Demo】

    写 JS 写习惯了,再写 Dart 确实没有那么爽。这个观点没有对错,确实是仁者见仁,智者见智了。布局上面,由于可以把 Flutter 的布局理解为 Css in Js ,因此,可以简单同理为写 RN 的布局。在 Flutter 的基础布局中,共有组件 31 个,您熟悉它需要一点时间成本,由于 UI 也延续了 App 的设计理念,因此 UI 定制化的灵活度,不如 Css3 那么狂拽炫酷吊炸天,但是,满足正常的业务诉求,还是没问题的。

结论:

Flutter To App 或 To Web, 页面的还原度非常高,大胆一点的话,确实可以部分场景进行商用。

02 开发计划

    心血来潮的列了一个计划,然后就开始干了。由于实践时间的问题,这篇文章不涉及性能优化的问题,理想很丰满,现实是,确实还没时间开始做。本篇文章只专注体验了 UI 层面的还原的实现,后续进行性能优化的经验后,再跟大家一起分享。

    开发过程中遇到了很多不知所云的问题,导致实际的速度没有那么快。后面精简了很多功能,索性差不多简单做完 3个 一级页面的展示,做了一个基础版本的 Demo,如果要精细化的还原 UI,还是需要下不少功夫的。

    简单说一下做的流程,先把页面拆了,划分成不同的区域。一个层级、一个模块的进行组件的拆分和整理(上图简单的列了一个开发时的中间状态,过模块的时候,查看基础组件的能力,是否满足页面 UI)。拆完之后,学习一下基础组件的使用(之前看过一点,确实过完节就忘记),然后分别拆分实现,最后组合安装成页面的 UI 和功能。单纯从 UI 这个角度上,写 Dart 跟写 HTML 和 CSS 差不多,但确实没有在浏览器开发那么爽。

    样板工程里面,并没有很在意代码规范,文件写的乱了,才能体会到规范的重要性。前期确实什么也不会,有一些都是网上搜索功能,然后粘代码测试,时间长了以后,写的多了的时候,才开始关注如何能写的更好。业务在落地的时候,还是要制定一套代码设计规范,这对团队很重要。一套好的代码规范,可以提升很多开发效率

您有好的 Flutter 开发规范的设计思路,欢迎在留言区域讨论。

03 实例拆解

    比较核心的几个点就是 底部状态栏、顶部导航栏、轮播图切换、路由状态维护。下面我们分别从前端角度,介绍一下开发过程中的体验问题。在跨端的技术方案的进程中,大概率发生的事情就是,如果 Flutter 发展起来了,未来前端会加入进来,参与到工程化和业务开发中。而 Native 下沉到 基础组件 和 底层核心库 的性能优化,就类似的理解就像后台服务把接入层交给 Nodejs 去处理,而 C++ 专注做算法和数据中台。

    从目前看客户端做页面短期内是没问题,但当技术进入深水区的时候,让客户端写页面确实有点糟蹋人力。专注做底层 框架 和 SDK 的设计才是核心价值;而在工程化的方向上面,前端就有更大的发挥的空间了。下面我们分别从几个方面来看待 Flutter 开发过程的是与非。

01

语言层面

代码语言:javascript
复制
import 'dart:convert';
import 'package:meta/meta.dart';

class SysCourse {
  final String title; // 标题
  final String timeArea; // 上课时间

  SysCourse({
    this.timeArea,
    this.title,
  });

  static List<SysCourse> fromJson(String json) {
    List<SysCourse> _sysCourseList = [];
    JsonDecoder decoder = new JsonDecoder();
    var mapdata = decoder.convert(json)['List'];
    mapdata.forEach((item) {
      SysCourse obj = new SysCourse(
        timeArea: item['time_area'],
        title: item['title'],
      );
      _sysCourseList.add(obj);
    });
    return _sysCourseList;
  }
}

类似这样的使用
var sys = new SysCourse(title: '高一秋季系统课', timeArea: '9月10-12月26日');

    Dart 是静态语言,如上面一段代码就是对一个系统课的对象,进行定义和初始化。如果您写过 C++ 或者 Java 的话,理解起来会非常简单。构造函数可以方便您初始化对象,函数的继承采用单一集成的方式,不像 C++ 那样可以同时继承于多个类。但是可以采用混入 mixins (with进行扩展)。已经存在继承,当然也肯定存在 override 复写,下面摘抄了一段剪短的官方代码(

https://dart.dev/guides/language/language-tour#instance-variables):

代码语言:javascript
复制
class Person {
  String firstName;

  Person.fromJson(Map data) {
    print('in Person');
  }
}

class Employee extends Person {
  Employee.fromJson(Map data) : super.fromJson(data) {
    print('in Employee');
  }
}

main() {
  var emp = new Employee.fromJson({});
  if (emp is Person) {
    emp.firstName = 'Bob';
  } else {
    (emp as Person).firstName = 'Bob';
  }
}

// 输出:
// flutter: in Person
// flutter: in Employee

    如果您很少写静态语音,用 dart 开发 您可能还是要适应一下,我也是长时间的写了 2-3 天之后,才开始慢慢适应的。现在写 JS 又有点慌了,哈哈,确实尴尬了,代码能力还不是很到家,要继续提升。

    因此,这里也引申出了一个问题,技术本质上还是需要沉淀的,专一是一件很重要的事情,即使语音范畴也是一样的。一个 JS 闭包的设计,也许一个技术专家能跟你聊一上午。从设计原理,到实现思路,以及优缺点。因此,很多时候,多而不精确实也是一个问题!但这个问题,因人而异,也因环境而异,看个人和团队选择了。行业人才,即需要单一方向有深度的,也需要横向上有广度的。因为,站在不同的维度,看到的世界是不一样的。

    无论是公司或者团队的技术建设,还是产品规划,不要为了统一而统一,到时候因为大一统了,反而少了试错的可能,很可能导致一次错误的决定,就全部都玩完了。这就搞笑了!我一直都是一个多元化的倡导者,多种不同观点和认知的存在,才能有更多创新的可能。当然,多元 不等于 不聚焦!

02

UI层面

    核心组件的拆分其实和平时写页面一样,先关注一下大的功能模块;之后进行组件划分;再然后进行页面 UI 元素的绘制,以及逻辑的编写,当然这些都是一些常规操作了。

    但由于第一次写,所以,根本没有办法按照套路出牌。开始的时候,大量粘贴了网上的代码。随着熟练之后,才开始慢慢手写。因此工程中的代码,有非常多的冗余和设计不合理的地方。后续,有时间了可以把代码进行重构和优化。历史包袱很多时候,都是新人搞出来的事情。你是不是似曾相识了,发现团队里面一个非常重要的项目,最开始的设计居然是实习生搞的!后来,一堆所谓的高级工程师给这个项目补锅,然后说自己是如何补锅,痛骂前任代码垃圾!

    实习生说 —— 这个锅,我不背~~~

    亲,就我现在这代码要是合到了 APP 的发布流里面,不用过半年,活脱脱的就是历史包袱了我估计是没时间优化这个工程的代码,真是因为想快速测试结果,才导致了细节的丢失,看场景,我这个场景,只是为了学习,不会任何商用,这里就不讨论对错了)。

 但无论如何 —— 规范很重要!哪怕开始的时候慢一点。但是总有产品说,这个需求必须下周三上线,这个是宇宙无敌第一需求,这是 XXX 提的。开发估计想说,XXX 你妹啊。这代码过了半年以后,就是锅。都要开发 Leader 自己背!开发 leader 就是背锅侠,你不背谁背,你就是干这个的。所以规范和流程很重要。专注于 —— “ 规矩的开发 ”,是工程师和码农之间最大的区别,也是我们成长的必须课。要学习写干净整洁的代码!

    Flutter 常用的 布局组件有 如 单子 widget 的 Container、Padding、Center 可以作为排版布局的基础元素。比如上面的卡片,就可以用 Container 进行包裹。而多子 widget 的情况下,可以使用 Row 和 Column,以及类似 Flex 布局的 Expanded 进行处理。层叠样式 可以用 Stack 和 Positioned 进行处理。比如下面红色区域,即可以用 Stack 处理,也可以用 Row 进行排版。

03

路由导航

代码语言:javascript
复制
@override
  Widget build(BuildContext context) {
    Map<String, WidgetBuilder> routes = {
      'pack': (BuildContext context) => CoursePack(),
      'my': (BuildContext context) => My(),
      'break': (BuildContext context) => Break(),
      'router': (BuildContext context) => DiscoverRouter(),
      'my': (BuildContext context) => My(),
      'grade': (BuildContext context) => Grade(),
      'discover': (BuildContext context) => Discover()
    };

    return MaterialApp(
      title: '企鹅辅导',
      theme: ThemeData(
        primaryColor: Colors.white,
        primarySwatch: Colors.blue,
      ),
      debugShowCheckedModeBanner: false, // 隐藏右侧 BUG
      home: SplashPage(), // 默认进入闪屏
      routes: routes, // 路由表
      style="margin: 0px; padding: 0px; font-size: inherit; line-height: inherit; color: rgb(128, 128, 128); overflow-wrap: inherit !important; word-break: inherit !important;">// 路由表找不到,在进入此路由处理
      navigatorObservers: [routeObserver],
    );   
 }

上面的代码您可以看到 routes 里面的路由对照表,返回一个对应的路由页面。

代码语言:javascript
复制
class Goto {
  static Goto shared = Goto();

  void goto(BuildContext context, time, String router) {
    Future.delayed(Duration(milliseconds: time), () {
      Navigator.pushNamed(context, router); // 路由跳转
    });
  }
}
// 调用如下方式,进行页面跳转
Goto.shared.goto(context, 100, 'subject');

这里没有用到路由的传参,传参的案例 代码在 example 工程里有用例子,可移步 example 工程查看。

代码语言:javascript
复制
 _navigateToProductDetail(BuildContext context, Product product) async {
    this.result = await Navigator.push(
      context,
      MaterialPageRoute(
        builder: (context) {
          // 带着参数,打开一个新的页面
          return new ProductDetail(product: product);
        },
      ),
    );

    print('result list = ' + this.result.toString());
 }

 Navigator.pop(context, ['Fred Wu', product.desctiption]);

04

依赖管理

    Flutter 的资源管理在 pubspec.yaml 文件中进行统一的管理。有一些相关规则,大家进行实际开发的时候,可以详细了解一下。比如 设备像素比 的匹配规则,字体库的加载,图片资源的管理等。在 Flutter 中也有类似 Npm 的包管理器,它用的是 pub。flutter pub get 进行可以进行项目依赖的下载。

05

事件交互

您看到了,页面有一些点击和滑动操作。Flutter 提供了强大的事件监听能力 —— Pointer Event 和 Gesture Detector。他们的使用跟我们在 JS 中使用事件监听的方式差不多。下面就是轮播图内嵌的图片点击事件监听,点击之后会打开一个 webView。

代码语言:javascript
复制
GestureDetector(
  child: Container(
    margin: EdgeInsets.all(10.0),
    child: ClipRRect(
      borderRadius: BorderRadius.circular(5.0),
      child: Image.asset(
        item,
        fit: BoxFit.cover,
      ),
    ),
  ),
 >    print('轮播图点击');
    Navigator.pushNamed(context, 'webView'); // 路由跳转
  },
);

    针对于复杂的用户交互 Flutter 引入了 Arena 的概念,在我的理解就是 battle ,来较量一下,最后,只会有一种事件进行业务处理。这里您在实际开发中就会有所体验,如果想多重事件共同发生,就要您定制化的实现了。

06

数据通信

常规的组件传递就和 React 的开发类似了,Vue 里面是存在事件代理的概念。当然您如果想在 React 内实现它,也不是一件复杂的事情,我们只是在规范和灵活之间做一些取舍罢了。可以把它简单理解为事件总线,进行事件的订阅和分发,帮助您进行跨组件的事件通信,减少多层级传参的代码负担。

代码语言:javascript
复制
EventBus eventBus = new EventBus();
class TransEvent {
  String text;
  TransEvent(this.text);
}

触发事件,发送通知。

代码语言:javascript
复制
eventBus.fire(TransEvent('gradeRouter'));

事件监听,收到事件触发之后,进行状态处理,展示年级页面。

代码语言:javascript
复制
eventBus.on<TransEvent>().listen((TransEvent data) => change(data.text));

07

生命周期

    在关注生命周期的时候,不要忘记 APP 的生命周期。这是作为前端同学比较容易忽略的。这一部分内容,在之前的一篇文章中有所提及,您可以点击观看生命周期的部分。

    在实际开发中,有一点值得注意的是:initState 表示当前 State 将和一个 BuildContext 产生关联,但是此时 BuildContext 没有完全装载完成!如果你需要在该方法中获取 BuildContext ,可以使用 Future.delayed(const Duration(seconds: 0, (){//context}); 进行处理。

    这里与 React 类似,防止内存泄露是很重要的。开发的时候也遇到了类似的 告警泄露的问题(https://stackoverflow.com/questions/52130648/nosuchmethoderror-the-method-ancestorstateoftype-was-called-on-null),因为没有释放掉对象初始化的内容。因此,要么使用单例模式,要么需要在生命周期函数中进行数据释放。

代码语言:javascript
复制
class Foo extends StatefulWidget {
  @override
  _FooState createState() => _FooState();
}
class _FooState extends State<Foo> {
  StreamSubscription streamSubscription;
  @override
  void initState() {
    super.initState();
    streamSubscription = Bloc.of(context).myStream.listen((value) {
      print(value);
    });
  }

  @override
  void dispose() {
    streamSubscription.cancel(); // 释放对象内的变量
    super.dispose();  
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

08

Flutter to Web

    上次转上课页的时候,有非常多的边距无法看清。这次我写的时候,稍微注意了 Flutter 的排版格式。转换出来的还原度已经非常高了,比上一次上课页的转换效果还要好一些。上面两张图,您会发现标题的位置,最开始的时候 Web 版本是没有居中对齐的。可以通过更换 widget 的结构进行优化,就可以看到,后面变成了居中显示的版本,你可以看下面的代码。

    从目前 Flutter to Web 的表现看,有些超出预期,在兼容方面的处理也是 小于 RN to Web 的。

04 Todo

打包对目前来说,意义不是特别大。业务目前不会发布 Flutter 的独立 App 版本。而 组件化 和 工程化 是目前需要专注的部分,欢迎一起讨论,共建开发体验。

05 后记

    整体开发体验进行到现在,还是非常有意思的。后面有时间把网络请求的数据存储都接入进来,这里比较麻烦的事情是在 App 内都是有登录态的,因此技术方案,还是需要结合 App 去落地,单纯的全靠 Flutter 支撑业务,效率还是不够高。比如:要用 Dart 独立完成一套接入手Q 和微信的登陆体系,以及支持自由账号体系的手机号登陆,仅仅是做完一套端的登陆体系接入,就是一件很重的体力活。因此,能力能复用的就坚决复用,能借鉴的就赶快借鉴!团队服务好业务是核心目标,团队的技术成长只是达成目标的手段之一

    后面要和客户端同学共同开发,一起去完成 Flutter 的企鹅辅导的业务的体系化探索实践。所以,有致力于开发 Flutter 的同学,以及已经在 Flutter 的道路上前行的同学,可以私下@我,作为 Flutter 萌新,可以跟你一起探讨技术,共建内部开发者社区,一起把更好的产品体验,回馈我们的用户。安心做好产品,服务好用户,是我们作为业务团队的核心价值

IMWeb 团队隶属腾讯公司,是国内最专业的前端团队之一。

我们专注前端领域多年,负责过 QQ 资料、QQ 注册、QQ 群等亿级业务。目前聚焦于在线教育领域,精心打磨 腾讯课堂、企鹅辅导 及 ABCMouse 三大产品。

扫码关注 腾讯IMWeb前端团队

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-10-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 腾讯IMWeb前端团队 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • IMWeb 团队隶属腾讯公司,是国内最专业的前端团队之一。
相关产品与服务
事件总线
腾讯云事件总线(EventBridge)是一款安全,稳定,高效的云上事件连接器,作为流数据和事件的自动收集、处理、分发管道,通过可视化的配置,实现事件源(例如:Kafka,审计,数据库等)和目标对象(例如:CLS,SCF等)的快速连接,当前 EventBridge 已接入 100+ 云上服务,助力分布式事件驱动架构的快速构建。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档