导语:腾讯在线教育团队(简称: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前端团队