
Flutter在58App上的深度调研
背景
现在跨平台的框架主要有如下几种:
- ReactNative,Weex
 - kotlin-native
 - Flutter
 - 小程序
 - Hybrid
 
长期来看,跨平台开发一定会是一个趋势,因为其能带来如下好处:
- 减少开发成本,提升开发效率
 - 动态部署,不依赖发版
 
但现阶段,框架很多,各有各的优缺点,对于应用开发的RD来说,面临一个框架如何选择的难题。在行业趋势没有真正出现之前,RD应该要勇于去学习,去尝试新框架,学习其设计思想,体验其优势与劣势,找到最适合自己的框架。
之前对Flutter做过简单应用的尝试(Flutter实现Git权限分配工具之旅),但不够深入,任何一个框架在没有真正进行深入实践时,根本无法判断其优缺点,为了不浮于表面,人云亦云的去判定Flutter框架,才有了这次的调研:基于Flutter实现58App的首页功能(首页模块是58App相对比较复杂的模块)
具体实现
首页tab框架
实现效果
在Flutter的Material Widget里,有BottomNavigationBar和TabBar两个类似的效果,但都无法直接使用,改造成本非常的大,最终选择自定义实现底部栏。
自定义ImageButton Widget
ImageButton的要求:
- 支持图片与文本
 - 支持两种状态:default,active
 - 不同状态有不同的图片,不同的文本颜色
 
实现思路:
- InkResponse Widget实现处理点击事件
 - Column布局
 - StatelessWidget,通过props来修改状态
 
1  | import 'package:flutter/material.dart';  | 
自定义HomeBottomNavigationBar Widget
要求:
- tabItem数量为奇数,中间的发布大小凸出来
 - 能与TabBarView联动
 
实现思路:
- Container Widget设置高度,背景
 - Row,Expanded做等分
 - Padding设置每个tabItem的paddingTop
 - 通过TabController实现与TabBarView联动
- tabController 继承 ChangeNotifier,ChangeNotifier是用于通知观察机制
 - _controller.addListener()来监听TabBarView的切换
 - _controller.animateTo(i)来通知tab的切换
 
 
代码如下:
1  | import 'package:flutter/material.dart';  | 
首页tab
实现思路:
- Stack Positioned实现叠层布局,解决tabbar凸起部份覆盖在TabBarView上
 - TabBarView Widget实现类似ViewPager效果
 
代码如下:
1  | import 'package:flutter/material.dart';  | 
内嵌ReactNative
实现思路:
- 通过独立的Flutter Plugin实现
 - ReactNative的ReactRootView可以被嵌入Native中,那同样可以被嵌入Flutter中
 - Flutter的AndroidView只有两个状态:create,dispose。在这两个状态里,执行ReactNative相关的生命周期函数
 
dart部分:创建对应的Widget
1  | class WubaRNView extends StatefulWidget {  | 
Android的实现:
- 注册ViewFactory
1
2
3
4
5
6
7
8public class WubarnPlugin {
public static final String VIEW_TYPE = "plugins.wuba.com/wubarnview";
/** Plugin registration. */
public static void registerWith(Registrar registrar) {
registrar.platformViewRegistry().registerViewFactory(VIEW_TYPE, new WubarnViewFactory(registrar.messenger()));
}
} - 通过ViewFactory创建WubarnView
1
2
3
4
5
6
7
8
9
10
11
12
13public class WubarnViewFactory extends PlatformViewFactory {
private final BinaryMessenger messenger;
public WubarnViewFactory(BinaryMessenger messenger) {
super(StandardMessageCodec.INSTANCE);
this.messenger = messenger;
}
public PlatformView create(Context context, int id, Object o) {
return new WubarnView(context, messenger, id);
}
} - WubarnView的具体实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44public class WubarnView implements PlatformView, MethodChannel.MethodCallHandler{
private final ReactRootView mReactRootView;
private final ReactInstanceManager mReactInstanceManager;
public WubarnView(Context context, BinaryMessenger messenger, int id) {
MethodChannel methodChannel = new MethodChannel(messenger, WubarnPlugin.VIEW_TYPE + "_" + id);
methodChannel.setMethodCallHandler(this);
// ReactNative的创建及初始化,设置其默认加载的bundle名称
mReactRootView = new ReactRootView(context);
mReactInstanceManager = ReactInstanceManager.builder()
.setApplication((Application) context.getApplicationContext())
.setBundleAssetName("index.android.bundle")
.setJSMainModulePath("index")
.addPackage(new MainReactPackage())
.setUseDeveloperSupport(false)
.setInitialLifecycleState(LifecycleState.RESUMED)
.build();
// 这个"App1"名字一定要和我们在index.js中注册的名字保持一致AppRegistry.registerComponent()
mReactRootView.startReactApplication(mReactInstanceManager, "App1", null);
}
public View getView() {
return mReactRootView;
}
public void dispose() {
// mReactInstanceManager.onHostPause(mActivity);
// mReactInstanceManager.onHostResume(mActivity, null);
// mReactInstanceManager.onHostDestroy(mActivity);
mReactRootView.unmountReactApplication();
}
public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
switch (methodCall.method){
case "":
break;
default:
result.notImplemented();
}
}
} - 上面初始化ReactInstanceManager当中的常量,与React代码是一一对应的
- “App1”:与在React里注册的组件名称是一样的
1
2
3
4import { AppRegistry } from 'react-native';
import App from './App';
AppRegistry.registerComponent('App1', () => App); - .setJSMainModulePath(“index”):JS bundle中主入口的文件名,是React工程里的入口文件index.js的名称
 - .setBundleAssetName(“index.android.bundle”):这个是内置到assets目录下的bundle名称,与bundle生成命令有关
1
react-native bundle --platform android --dev false --entry-file index.js --bundle-output /Users/ly/liuyang/workspace_flutter/wubarn_plugin/example/android/app/src/main/assets/index.android.bundle --assets-dest /Users/ly/liuyang/workspace_flutter/wubarn_plugin/example/android/app/src/main/res/
 
 - “App1”:与在React里注册的组件名称是一样的
 
发布入口页
实现效果
切换效果
实现思路:
- 通过PageRoute,去掉切换的动画
 - 通过AnimatedBuilder,实现旋转动画
 - 通过WillPopScope Widget拦截返回事件
 
Flutter的页面切换是由Navigator管理,其中有一个栈,栈帧是路由,通过PageRoute可以自定义切换的动画,如下去掉切换动画的代码:
1  | Navigator.push(context, PageRouteBuilder(  | 
由于Flutter是MVVM框架,Flutter里的Animation只负责计算,不负责界面布局与渲染,需要手动调用setState()来让界面重绘,不过可以通过AnimatedBuilder简化流程,但Flutter在实现组合动画比较麻烦。
1  | class PublishHome extends StatefulWidget {  | 
渐变按钮
要求:
- 不使用图片实现
 - 背景支持渐变
 - 不要点击效果
 
Material Widget里的四种Button无法满足按钮要求,第三方渐变按钮也无法完全满足要求,通过Container Widget的decoration自定义此Widget:
1  | import 'package:flutter/material.dart';  | 
部落图片选择控件
实现效果
底部抽屉效果
要求:
- BottomSheet增加中间态
 - 有回弹效果
 
第三方库RubberBottomSheet实现了此效果,其原理如下:
- 通过Stack实现叠加布局
 - 修改AnimationController的原码,依据lowerBound,upperBound的实现思路,实现halfBound,即中间态
 
直接使用RubberBottomSheet的代码非常简单:
1  | class TribePublish extends StatefulWidget {  | 
加载并显示相册图片
加载相册图片
- 通过MethodChannel,实现与Native通信,加载相册图片
 - 在Android里,加载相册图片,需要先授权
 - 防止相册图片过多,需进行分页加载
 
Android端的代码实现:
1  | public class AlbumManagerPlugin implements MethodChannel.MethodCallHandler {  | 
Flutter端的代码实现:
1  | class AlbumManagerPlugin {  | 
细节点:
- Native的扩展能力定义为Plugin,Plugin可以独立发布为一个库,里面即有native代码也有dart代码,不用像ReactNative,需要单独合并native的代码,但带的问题是:dependencies库都是直接原码
 - 通过MethodChannel进行Flutter与Native通信,可以传递参数,如何传递一组参数了,通过源码分析:Map对象
 
分页显示图片
- 通过GridView显示图片,实现分页加载
 - 默认的图片加载策略是LRU,体验与内存表现都很不好
 
下面的代码没有实现分页与图片加载策略的优化:
1  | class AlbumGrid extends StatefulWidget {  | 
结论
Flutter框架在设计上,整体优于其他跨平台框架,实现使用时,也是非常的方便,有如下感受:
- 开发调试非常的快,比Android的instant run强很多,也稳定很多
 - dependencies依赖管理比ReactNative强,native扩展能力是一个独立的plugin库,便于管理依赖
 - 基于MVVM框架,在自定义UI组件及动画方面,结构清楚,容易理解
 - 实现相同的功能,代码量远小于使用java实现
 
由于Flutter的社区不太完善,时间太短,生态不完善,相当于2011年开发Android一样,缺少大量成熟的基础库,大量的基础能力都需要从头到尾开发,下面是上述实践过程中发现的一些点:
- 渐变Button,图片Button
 - GridView或ListView的图片加载策略(Fling时不加载,scrolling或idle时加载)
 - 崩溃日志收集
 - 大量的基础Plugin:加载相册,授权,地图,视频等等
 - …
 
在已经集成ReactNative的58App里,已经基本满足部分业务的动态能力,再花大量的成本完美Flutter的基础,花大量的成本去推动业务线使用,短期来看,投入产出比太低。
但从长期来看,在跨平台框架上,我更加看好Flutter,在设计与使用体验上,Flutter确实都优于其他框架,但Flutter最终能否成为主流,还是要看Google的推广力度。
持续关注跨平台框架的动态,ReactNative也在向Flutter学习,改进其性能差的一面,Flutter的基础库也在不断的完善中
此demo的代码:wuba_gallery