本文是Flutter系列的第一篇文章,主要介绍开眼快创在Flutter跨端实现上的一些实践,以及在过程中遇到的问题和解法,同时也包括令开发者们比较头疼的状态管理、数据通信等。在后续文章中,我们会继续详细讲述Flutter在开眼快创的音视频实践、优化实践以及研发支撑,希望可以为广大开发者们提供一些参考,请持续关注。
随着跨端方案技术的不断演进,Flutter作为新一代自渲染引擎跨端方案,也随之受到了越来越多业内开发者的青睐,并且因为官方以及社区开发者持续对其版本进行迭代与优化,解决了大部分的性能和易用性问题,所以,Flutter也快速进入了 2.0时代。开眼快创[1]是围绕商业化广告创意构建的一款产品,其目标人群涵盖了供应商、代理商、广告主、商家号以及视频行业等从业者,产品的目标是提供智能化生产素材相关的产品能力,从而达到降低商业属性用户生产(创意制作)门槛的目的,大幅提升广告创意质量,提高素材消耗。它的主要功能包含:创作:模板视频、视频编辑、一键大片
创意灵感:精选案例、快手热门、优质广告
创意学院:创意课程、课程直播等
数据分析:广告素材投放数据报表等
开眼快创的UI展示部分是全部使用Flutter开发的,除了普通页面例如列表页外,基于Flutter,我们实现了在数据分析场景下的各种图表绘制。同时在音视频领域,我们也落地了一些功能场景,例如视频直播、视频编辑、模板视频等。本文主分享的是开眼快创团队在Flutter上的实践。
开眼快创是纯粹用Flutter开发的项目,其整体采用组件化分层架构,如下图:最底层是平台嵌入层,目前只有Android和iOS。在Flutter 2.0版本发布后,平台嵌入层这里可以有更多想象的空间。通信层则提供了平台层和Flutter App之间的通信能力,除了比较熟悉的Platform Channel和外接纹理以外,我们在图片处理和视频编辑的功能中使用了性能更高效的Dart FFI,将在后文中详细介绍。在Flutter应用的最顶层是App工程,也就是开眼快创App。中间层是业务层,目前还没有抽取到独立库,未来可能会考虑把视频编辑抽取成独立的业务库。下面是基础组件层,它封装了独立的通用库用来向上提供基础能力,通过Pub远程依赖,降低项目耦合度,同时也可以反哺Flutter平台建设,降低其余Flutter项目的开发成本。目前已经沉淀10+个通用组件,并发布4个组件到移动开发者中心。Flutter本身可以独立处理网络请求、文件读写以及UI绘制等主要功能,除了音视频需要原生参与,大部分业务场景均由Flutter独立完成,整个App的业务代码90%以上由Flutter来实现,原生基本不需要参与业务开发。纯Flutter开发可以保证:UI 统一
功能统一
逻辑统一
以上几点的统一,对于业务开发,特别是在复杂业务场景下(如视频编辑),可以做到双端基本完全一致,这就省去了双端逻辑对齐和UI对齐的成本。同时也不用管理混合栈,因为所有的页面都在Flutter端,了解Flutter开发的同学应该都知道维护混合栈的管理非常麻烦,且目前市面上已有的混合栈管理方案也都存在需要持续适配新版本 Flutter的问题,所以使用纯Flutter开发不论是在开发阶段,还是在后期维护阶段,成本都比纯原生开发要小很多,且总体开发效率提升了一个档次。除此之外,使用纯Flutter开发对性能也更加友好,原生部分不会占用太多的内存,开眼快创在大量音视频模块的应用场景下也并没有遇到内存瓶颈,所以纯Flutter开发对于对性能更有保障。
众所周知,Flutter的UI是声明式的,根据当前状态,唯一可以确定的是当前UI的样式。Widget是声明式的 UI 结构,同时也是面向开发者的接口,它是对UI的描述性表达,即对描述Element的配置的。在Flutter应用中,开发者可以通过组合Widget构建出想要的UI效果。Widget 是不可变的(Immutable),这就意味着每次刷新都会重新构建出新的Widget对象,创建的开销很小,成本较低。我们通常将Widget组合构建出的UI层级结构称为Widget Tree,但相比Element Tree,实际上并不存在Widget Tree,由于Widget节点挂载在Element节点上,所以我们可以将其抽象为Widget Tree。当State变化时会引起Widget Tree重新构建,进而将State状态数据通过Element Tree刷新到RenderTree中对应的RenderObject中,最终改变布局和渲染效果。相较于原生开发的命令式构建UI视图,声明式的Flutter只需管理好App的State状态,就可以构建出想要的UI视图。需要注意的是,声明式UI和数据绑定更新视图,有本质上的区别,不要混淆,其状态基本可分为两种,一种是页面内的状态,一种是跨页面共享的状态。对于页面内的状态,由于不需要跨页面共享数据,所以只需要关心数据流向及业务逻辑。开眼快创选择了BLoC作为页面内状态管理,BLoC模式的核心在于它定义了数据的流向,分离了业务逻辑与视图逻辑,如下图:BLoC内部封装了业务逻辑,通过Stream触发State变化,进而更新UI样式,所以BLoC可以实现从Produder到Consumer的数据流向。同时,配合RxDart使用还可以简化BLoC模式的构建过程,可以提供更丰富的能力。但是,使用BLoC也并不是绝对的,有时在一些简单的页面或者不依赖外部服务的页面中,使用BLoC就会显得比较笨重,所以这种情况下就可以考虑其它轻量方案,如ValueNotifier等。有一点需要注意的是BLoC模式的拓展性较差,不适合页面间状态管理。对于简单的跨页面共享数据状态,可以选择InheritedWidget或Provider,实际上,Provider也对InheritedWidget做了封装。对于复杂的业务来说,如视频编辑,这些状态管理工具会有点力不从心。所以我们选择了Redux,Redux是前端领域的应用状态管理框架,其有三个核心原则:单一数据源Store;
State是只读的(Immutable);
数据改动须是纯函数(这些纯函数叫Reducer,定义了如何修改Store,由Action 触发)。
使用Redux可以很好地对业务模块进行解耦,同时也可以对功能逻辑和业务视图进行分离,使整体更加清晰。Redux可以保证数据流是单项的,通过Middleware中间层,可以对数据流做中间逻辑处理,也保证了数据源是可追踪的。整个视频编辑模块都是基于Redux来实现的,通过分离逻辑和视图,整体开发效率更高,架构也更清晰。Flutter_redux[2]是Redux在Flutter平台的实现。由于js和Dart语言本身的特性差异,导致了Flutter Redux实现上会出现一些问题:第一个问题:由于js是动态的,在React-redux中,全局的State只是一个Object,对业务无耦合,各业务通过各自的Reducer在State里添加数据即可;而在Flutter-redux中,这个State必须是个强类型,也就是需要显式地定义所有的字段,这里产生了跟业务的强耦合。
第二个问题:它的数据流是单向的,只能通过Action → Producer修改数据,这可以保证数据源的可控性以及大型项目的健壮性。在Redux中,Action触发数据更新,需要对旧Model进行拷贝,以判断是否需要触发更新操作,在js中可以很容易通过解构操作符浅拷贝一个对象,比如:
newState = {
...oldState,
someKey:newValue
}
但在Dart中,没有这样的语法操作,虽然这样对数据变量较少的Model没有太大影响,但如果是很多变量的复杂Model就需要挨个字段手动赋值,操作很繁琐并且容易出错。针对这个问题有以下解决方案:
class AppState {
final int counter;
final String name;
AppState({this.counter = 0, this.name = "John"});
AppState copyWith({counter, name}) {
return new AppState(
counter: counter ?? this.counter,
name: name ?? this.name
);
}
}
@override
AppState rebuild(void Function(AppStateBuilder) updates) =>
(toBuilder()..update(updates)).build();
同时,也会生成Equals方法,用于比较数据,可以控制是否刷新,用于提高性能。Built_redux[4]基于Built_value做了更进一步优化,将Action、Reducer、Middleware的绑定也做了模板化生成,他们之前的绑定关系由框架来生成。开发者只需要关心数据变化,而不用关心Reducer是如何根据Action生成不同的State的。另外,绑定关系也可以确保数据类型的安全,这也间接地解决了类型绑定的问题。所以,Built_redux可以被理解为两层意思:
绑定 Redux 中State、Model、Action、Reducer
生成Built_value Model(非必须),这一步主要是为了解决上面提到的问题,对于已有的Model,也可以直接用在Built_redux中
基于以上考量,使用Built_redux可以作为Flutter Redux的实现。但是,Built_redux需要手写一些模板代码,比如BuiltClass、EnumClass等,那么该如何提效?其实,可以通过使用AndroidStudio Live Template模板代码,找到对应的设置目录,添加新的Template后,再把对应的模板代码贴进去,设置对应的快捷键和描述即可。
总结一下,不管是页面内的状态管理,或者页面间的状态管理,如果使用不当就可能引发性能问题,所以尽可能更细粒度地拆分视图,缩小刷新范围,避免不必要Rebuild。
通信包含两个方面,数据通信和页面通信。数据通信在Flutter层表现为状态管理,在跨Flutter层和Platform层级间则表现为跨语言的数据交互及方法调用。页面通信就是路由跳转。官方提供的基础Platform Channel存在以下问题:Channel方法的注册和分发过程是分散的,无法很好地管控分发过程。
三端的模板代码是硬编码,需要手写,非常容易出错。
数据类型不安全,三端无法在编译时保证数据类型一致。
一般我们选择 json 作为序列化方案,但是在一些复杂数据交互场景下,编解码方式不能很好地支撑业务,如视频编辑等。
关于第一点,我们的一般做法是集中式注册 & 集中式分发调用,是比较常见的在Webview中JSBridge的实现。我们一般会采用分总分的结构:组件内部分别注册,编译时生成汇总代码、运行时集中式管理以及调用时处理逻辑分发。事实上,我们也实现了通用的Bridge,可以在与h5通信、与Flutter通信等通信的场景中使用。但在一些应用场景下,通用Bridge不能满足业务需求,所以开眼快创使用了基于gRPC&ProtoBuf的通信方案。ProtoBuf(PB)是谷歌推出的二进制序列化协议,相较于json,PB的性能更好。Proto文件定义了统一的IDL接口,可以根据接口定义生成三端代码(Dart、Java、OC)。这可以确保它的方法签名参数及Model的数据类型是安全的,并且可以很好地支持二进制数据与Model定义之间的相互转换。gRPC是一款高性能的通用RPC框架,它定义了基于C/S架构的进程间通信的实现,gRPC通过PB提供的Plugin机制来实现原生桩代码生成。总结一下,PB定义了数据的序列化方式,gRPC定义了方法调用的管理方式,所以使用PB&gRPC可以很好地解决上面提到的问题。首先,数据类型是安全的,因为代码是根据IDL定义在编译时的动态所生成的,三端数据类型保持一致,并且不需要开发者手写模板代码,这也就省略了手写硬编码的中间过程,避免最容易出错的环节。其次,它可以支持多语言,并且很好地支持二进制数据,这就磨平了平台之间的差异,在多语言通信时可以保证二进制数据在各个语言环境下的相互转换,在本系列后续文章会以视频编辑为例进行详细说明。它的分发过程是集中管理的,可以在这里做一些统一错误处理和拦截操作,比如打印日志、埋点等操作。另外,它是C/S架构的,基于此,开眼快创开发了GrpcX通信方案。该方案实现了全双工的C/S通信模式,从而解决了两个问题:同步调用,异步返回。
全双工通信,实现了Flutter作为服务提供者、Native作为客户端请求者,在一些场景下需要Native去调用Flutter方法的功能,比如获取App状态数据等。
目前我们已经将GrpcX [5]开源回馈社区,同时我们也将推送相关的技术性文章,敬请期待。Flutter官方提供的Navigator存在一些问题,但是因为我们的App是纯Flutter开发,所以我们参考了原生路由,实现了一套基Url的路由库KRoute。它配置灵活方便、功能强大,通过注解配置在编译期基于Build_runner生成路由表和拦截器。这就保证了它的跳转参数是类型安全的。同时,我们在其中统一了Flutter页面跳转的转场动画、页面别名以及拦截器方法等,以保证我们在业务场景下的表现统一。另外,支持http(s) schema,页面跳转支持降级跳转h5功能。本文是Flutter系列的第一篇文章,主要介绍了开眼快创在Flutter跨端实现上的一些实践,以及在过程中遇到的问题和解法,也包括令开发者们比较头疼的状态管理、数据通信等。希望可以为广大开发者们提供一些参考。下一篇文章将介绍Flutter在音视频领域的实践和经验,请持续关注。
[1]https://cc.e.kuaishou.com/
[2]https://pub.dev/packages/flutter_redux
[3]https://app.quicktype.io/?share=qoIhiSuc2uRmQ1aThdNG
[4]https://pub.dev/packages/built_redux
[5]https://github.com/KwaiAppTeam/GrpcX
快手主站技术部客户端团队由业界资深的移动端技术专家组成,通过领先的移动技术深耕工程架构、研发工具、动态化、数据治理等多个垂直领域,积极探索创新技术,为亿万用户打造极致体验。团队自2011年成立以来全面赋能快手生态,已经建立起业内领先的大前端技术体系,支撑快手在国内外的亿万用户。
在这里你可以获得:
提升架构设计能力和代码质量
通过大数据解决用户痛点的能力
持续优化业务架构、挑战高效研发效能
和行业大牛并肩作战
我们期待你的加入!请发简历到:
app-eng-hr@kuaishou.com