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