万坤, 5 年安卓开发经验, 16 年加入饿了么, 现任职饿了么资深安卓开发工程师, 负责饿了么物流安卓相关 APP 线上的高稳定运行.
前言
Flutter 在今年 6 月份发布第一个 Release 预览版以来, 开发热度呈现了井喷式的爆发. Github 上 Flutter 项目的小星星也已经涨到了 3.6 万了, 同时国内闲鱼团队已经将 Flutter 用到了业务中并上线运行. 可以说 Flutter 已经有了非常成熟的使用环境, 在我们团队内部大家也是跃跃欲试. 这里我选择了我们团队页面中一个比较轻量级的页面 - 设置页面, 完成了 Flutter 的开发和上线准备工作, 下面主要是分享一下这一次亲密接触的经验和心得.
混合开发
实际上我们如果想把 Flutter 引入到现有的业务中去, 就必然会涉及到 Flutter 和 Native 混合开发的问题, 尤其是 Flutter 的代码怎么引入到我们原有的工程 (实际上官方的 Demo 是一个纯 Flutter 的工程). 我这边参考闲鱼的做法, 在 Android 端实现的主要步骤如下:
1. 新建一个 Android 的 module 工程. 将此工程作为 Flutter 相关业务打包的工程, 最终输出 aar 供主工程直接依赖;
2. 将 Flutter 的 jar 包直接引入到 lib 目录下. flutter.jar 位于 [Flutter SDK 目录]/bin/cache/artifacts/engine,Flutter 官方只提供了四种 CPU 架构的 SO 库: armeabi-v7a,arm64-v8a,x86 和 x86-64, 但是目前我们使用的 SDK 大部分只使用了 armeabi 架构, 这里需要将 arm 目录下面的 jar 稍作改造, 主要是解压后将 armeabi-v7a 目录更名为 armeabi 后再打包, 可以通过以下的脚本实现:
- cp flutter.jar flutter-armeabi-v7a.jar
- unzip flutter.jar lib/armeabi-v7a/libflutter.so
- mv lib/armeabi-v7a lib/armeabi
- zip -d flutter.jar lib/armeabi-v7a/libflutter.so
- zip flutter.jar lib/armeabi/libflutter.so
复制代码
3. 新建一个 FlutterActivity. 这个 Activity 供 Native 页面跳转. 同时也承载了和原生通信以及页面 route 的功能, 主要代码如下:
- public class MyFlutterActivity extends FlutterActivity {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- FlutterMain.startInitialization(this);
- super.onCreate(savedInstanceState);
- GeneratedPluginRegistrant.registerWith(this);
- //flutter 和原生通信的 channel 实现
- CustomChannel.registerSettingsMethodCall(this, getFlutterView());
- }
- // 根据 pageId 跳到到相应的 flutter page
- public static void start(Context context, String page) {
- Intent intent = new Intent(context, MyFlutterActivity.class);
- intent.setAction(Intent.ACTION_RUN);
- intent.putExtra("route", page);
- context.startActivity(intent);
- }
- }
复制代码
4. 新建 Flutter 工程, 这里推荐把 Flutter 工程作为 Android 工程的一个 submodule.
5. 拷贝 Flutter 工程 build 产出物. flutter build 之后会生成一些字节码和资源文件, 在打包时拷贝到 assets 目录下供运行时使用. 我们可以在 Flutter 工程开发完成之后通过以下的脚本输出产出物到 Android 工程:
- # 这里涉及的目录可以视自己的工程结构而定
- echo "Switch workspace"
- cd ./flutter_module
- echo "Clean old build"
- find . -d -name "build" | xargs rm -rf
- flutter clean
- echo "Get packages"
- flutter packages get
- echo "Build release AOT"
- flutter build aot --release --preview-dart-2 --output-dir=build/flutter/output/aot
- echo "Build release Bundle"
- flutter build bundle --precompiled --preview-dart-2 --asset-dir=build/flutter/output/flutter_assets
- echo "Copy flutter product"
- cp -rf build/flutter/output/aot/isolate_snapshot_data ../flutter-lib/src/main/assets/
- cp -rf build/flutter/output/aot/isolate_snapshot_instr ../flutter-lib/src/main/assets/
- cp -rf build/flutter/output/aot/vm_snapshot_data ../flutter-lib/src/main/assets/
- cp -rf build/flutter/output/aot/vm_snapshot_instr ../flutter-lib/src/main/assets/
- cp -rf build/flutter/output/flutter_assets ../flutter-lib/src/main/assets
复制代码
这里也实现了一个小的脚本, 在 Flutter 代码修改后直接接入到工程中运行:
- ./script/build.sh #上面的 flutterbuild 脚本
- ./gradlew app:clean app:assembleDebug
- adb install -r app/build/outputs/apk/app-debug.apk
- adb shell am start -n me.ele.fluttermodule.sample/.MainActivity
复制代码
不过还是建议直接先在 Flutter 工程中调试完成后加入到主工程, 毕竟 Flutter 的 hot reload 还是挺方便的.
Route
混合开发中遇到的另外一个问题就是页面的跳转管理问题, 尤其是原生和 Flutter 之间的相互跳转, 涉及到 route 问题, 这里 Flutter 也做了很好的支持:
- void main() => runApp(new MyApp());
- class MyApp extends StatelessWidget {
- @override
- Widget build(BuildContext context) {
- return new MaterialApp(
- title: 'flutter demo',
- theme: new ThemeData(
- primarySwatch: Colors.blue,
- ),
- routes: <String, WidgetBuilder>{
- Pages.PAGE_HOME: (BuildContext context) => new HomePage(title: 'flutter'),
- Pages.PAGE_SETTINGS: (BuildContext context) => new SettingsPage(),
- },
- home: new HomePage(title: 'flutter'),
- );
- }
- }
复制代码
App 可以添加一个 routes 列表, 通过
- Navigator.pushNamed(context, routeName);
- Navigator.pop(context);
进行页面的跳转, 在 Flutter 内部进行页面的跳转没有任何问题, 但原生与 Flutter 之间的页面跳转其实遇到了这样的问题:
我们在一个 Flutter 工程中实现了多个页面, 他们不总是一个入口, 但是这里却只有一个入口, home 的参数怎么从 Native 端传进来呢?
查看 MaterialApp 的源码, 这里有一个 initialRoute 的参数, 他是 APP 中 Navigator 默认展示的页面, 而且这个参数接受从 Native 端传入.
- initialRoute: widget.initialRoute ??ui.window.defaultRouteName,
- String get defaultRouteName => _defaultRouteName();
- String _defaultRouteName() native 'Window_defaultRouteName';
从这段代码里面可以看到如果在 flutter 中 APP 没有设置 initialRoute, 就会从 Native 中获取. 这样我们就可以在 Native 端传入不同的初始页面, 在 Android 端代码可以这样实现:
- Intent intent = new Intent(context, FlutterActivity.class);
- intent.setAction(Intent.ACTION_RUN);
- intent.putExtra("route", page);
- context.startActivity(intent);
复制代码
IOS 中也有同样的设置 initialRoute 的部分.
布局
Flutter 中的布局是基于 Widget 的, 可以说一切皆 Widget. 系统给我们提供了大量已经实现好的 Widget, 基本上我们是在这些 Widget 的基础上做一些组合完成布局的. 不过这样的结果也导致了 Widget 的结构非常扁平, Widget 的种类异常繁多, 给上手带来一些难度. 在 Flutter IO 的目录 https://flutter.io/widgets/widgetindex/ 中, 系统帮我们罗列了大概有 146 个之多的 Widget 的类型, 这里我简单的就我这段时间使用比较高频的一些 Widget 谈一些自己的体会.
StatelessWidget 和 StatefulWidget
我们的布局组合大部分需要继承这两个 Widget. 从字面意义来说很容易区分, 一个是有状态的, 一个是无状态的, 但实际使用中却经常容易混淆. 可以说除非是一些写死的 icon, 基本上所有的页面节点都是有状态的, 都会涉及到样式文案等的更新, 主要是看这个 state 维护在哪里, 如果维护在父控件, 那么这个相关的子控件就是个无状态的. 下面以 CupertinoSwitch 为例简单的对两种 Widget 做一个说明, 也是我在实际使用过程中踩过的坑. CupertinoSwitch 是系统提供的一个 iOS 风格的 Switch 控件, 定义非常简单:
- class CupertinoSwitch extends StatefulWidget {
- const CupertinoSwitch({
- Key key,
- @required this.value,
- @required this.onChanged,
- this.activeColor,
- }) : super(key: key);
- final bool value;
- final ValueChanged<bool> onChanged;
- final Color activeColor;
- @override
- _CupertinoSwitchState createState() => new _CupertinoSwitchState();
- @override
- void debugFillProperties(DiagnosticPropertiesBuilder properties) {
- super.debugFillProperties(properties);
- properties.add(new FlagProperty('value', value: value, ifTrue: 'on', ifFalse: 'off', showName: true));
- properties.add(new ObjectFlagProperty<ValueChanged<bool>>('onChanged', onChanged, ifNull: 'disabled'));
- }
- }
- class _CupertinoSwitchState extends State<CupertinoSwitch> with TickerProviderStateMixin {
- @override
- Widget build(BuildContext context) {
- return new _CupertinoSwitchRenderObjectWidget(
- value: widget.value,
- activeColor: widget.activeColor ?? CupertinoColors.activeGreen,
- onChanged: widget.onChanged,
- vsync: this,
- );
- }
- }
复制代码
它是一个 StatefulWidget, 实际上我们看到这个_CupertinoSwitchState 是没有维护任何信息的, 使用的参数都是 Widget 的, 所以说他也可以是一个 StatelessWidget. 这里我曾经也犯过一个错误, 我在 CupertinoSwitch 基础上封装了一个 CheckBox, 维护了一个 checked 的 state, 我在父控件中需要更新异步返回的数据对 CheckBox 进行刷新, 发现刷新无效. 原来是因为我只能刷新 Widget 的 checked, 而无法更新他的 state, 导致他的页面没有做更新. 实际上在开始写 flutter 的布局时经常会带着 Android 的开发思维陷入死胡同, 在 Android 经常我们通常是先完成控件的布局, 然后再找到这些控件对这些控件做刷新操作. 而在 Flutter 中, 数据都是维护在 state 中, 页面需要从 state 中取数据刷新, Widget 可以说都是临时的, 所以不要想着 find 到这个 widget 再调他的 updateState 这种逻辑了.
View 和 ViewGroup
这其实是 Android 中的概念了, 那在 Flutter 中有对应的东西吗? 对 Widget 根据 child 进行分类, 大概可以分成这几类:
1, 无 child. 这类 Widget 对应我们在 Android 开发中的基础 View, 基本上是页面展示的最基础的元素了, 像 Text,Image,Icon,Checkbox,Switch 等, 使用比较简单, 这里就不详细讲述了.
2, 单 child. 这类 Widget 对应我们在 Android 开发中的 style, 实际上是对 Widget 的一些样式的拓展, 在 Android 中我们通常是把样式作为 View 的一个参数, Flutter 中则是单独定义了很多 Widget 去支持这些样式. 这样也造成了很多嵌套, 实际上单 child 的这些 Widget 的多层嵌套并不会带来性能的损失. 多 child 的 Widget 则尽量减少嵌套.
Container. 这是使用比较广泛的一个 Widget, 它可以给 child 设置宽高, 背景, Margin,Padding 等.
Padding. 可以使用 EdgeInsets 提供的两种设置 padding 的方式, all 和 only.
Center. 子控件居中显示, 默认子控件布局是尽量大的.
Align. 设定子控件的对齐方式.
3, 多 child. 这类 Widget 对应我们在 Android 开发中的 ViewGroup, 涉及到页面的布局展示.
Row. 水平的 LinearLayout. 可以通过不同的主轴和垂直轴的对齐方式, 以及结合 Expand 控件, 实现非常复杂的 flex 布局效果.
Column. 垂直的 LinearLayout.
Stack.FrameLayout. 最普通的从左上角开始的布局, 子控件相互层叠.
Table. 表格. 可以实现丰富的表格效果.
ListView. 滚动的列表.
Card.Material Design 风格的 CardView.
问题
内存泄漏
在 iOS 端新开一个 Flutter 页面销毁后内存不会被回收, 导致内存会不断上涨至应用被杀, 应该是 iOS 端的一个 bug,Android 端没有出现, 后续的 Flutter 版本应该会修复, 当前需要做一些缓存的方式减少内存消耗.
黑屏
FlutterActivity 在初始化 FlutterView 的时候比较耗时, 会导致页面启动的时候黑屏, 好在 flutterView 提供了一个 addFirstFrameListener 接口, 看网上的方法是重写 oncreate 中的 setContentView 方法, 在首帧绘制完成前后控制一个 loading 层的显示, 查看 Flutter 源码也提供了官方的支持, FlutterActivityDelegate 会在 setContentView 之后添加一个 launchView, 而 launchView 是否显示是根据两个参数决定的:
- // 是否显示 lanchView
- private Boolean showSplashScreenUntilFirstFrame() {
- try {
- ActivityInfo activityInfo = activity.getPackageManager().getActivityInfo(
- activity.getComponentName(),
- PackageManager.GET_META_DATA|PackageManager.GET_ACTIVITIES);
- Bundle metadata = activityInfo.metaData;
- return metadata != null && metadata.getBoolean(SPLASH_SCREEN_META_DATA_KEY);
- } catch (NameNotFoundException e) {
- return false;
- }
- }
复制代码
- //lanchView 样式
- @SuppressWarnings("deprecation")
- private Drawable getLaunchScreenDrawableFromActivityTheme() {
- TypedValue typedValue = new TypedValue();
- if (!activity.getTheme().resolveAttribute(
- android.R.attr.windowBackground,
- typedValue,
- true)) {;
- return null;
- }
- if (typedValue.resourceId == 0) {
- return null;
- }
- try {
- return activity.getResources().getDrawable(typedValue.resourceId);
- } catch (NotFoundException e) {
- Log.e(TAG, "Referenced launch screen windowBackground resource does not exist");
- return null;
- }
- }
复制代码
对应的配置是在 manifest 的 activity 中添加一个 meta-data(注意是 Activity 的 meta-data, 而不是 Application 的):
- <activity
- android:name="YourFlutterActivity"
- android:theme="@style/FdAppTheme">
- <meta-data android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
- android:value="true"/>
- </activity>
复制代码
activity Theme 中添加一个背景:
<item name="android:windowBackground">@color/fd_background</item>
复制代码
这样就会在 flutterView 加载过程中显示 windowBackground, 如果想实现更复杂的 lanchview, 也可以参照 FlutterActivityDelegate 的实现方式.
卡顿
android 端 debug 开发的时候页面显示非常卡顿. 我这边开发的一个简单的设置页面, 主要是一个 ScrollView 包裹着一个 Column,debug 模式下滑动卡顿, 打开 Flutter Inspector 也是看到 GPU 和 UI 双曲线飘红. 为了验证 Release 包的流畅性, 我们在 profile 模式下打开 Flutter Inspector, 看到 UI 曲线一直显示绿色, fps 也基本稳定在 60, 感观上也是操作比较流畅, 但是 GPU 曲线一直飘红, 看官方介绍 offscreen layers 对 GPU 的计算有很大影响, 因为涉及到频繁的调用 savelayer. 可以通过 checkerboardOffscreenLayers 这个参数判断页面有没有在屏幕外绘制.
- new MaterialApp(
- checkerboardOffscreenLayers: true,
- );
复制代码
我们这里有一个 ScrollView, 导致不可避免的产生了屏幕外的视图. 由此可见 Flutter 对于 ScrollView 的支持并不高效, 后续可以替换成 listview 提高重用性.
使用心得
开发 Flutter 将近两周的时间, 使用起来感觉比较得心应手, 生态可以说非常的健全了, Widget 及 Widget 的自定义拓展基本上能满足各种复杂页面的开发. 另外 Dart 语言可能是对 Java 开发来说最友好的 web 语言了, 而且 AndroidStudio 对它做了很好的支持, 基本上我们还是可以做到点击自动跳转以及 class 一键 import 了. 如果是一个新开的项目, 用 Flutter 实现确实能带来很大的生产力的提高.
规划
目前对 Flutter 基本上只是一个大概的了解, 后续将从以下几个方面深入理解整个 Flutter 框架.
渲染流程 阅读 Flutter Engine 相关代码, 深入了解底层渲染的原理.
组件 网络, 本地通信, 存储, route 框架, 数据监控等基础模块的封装实现.
性能工具 Flutter 提供了大量的性能检测工具, 借助这些工具可以定位和优化程序中的性能问题.
命令解析 Flutter 提供了很多命令行实现编译和打包, 可以深入了解其中的实现原理.
代码架构 可以将 MVP,MVVM 等架构方案引入到 flutter 中.
阅读博客还不过瘾?
来源: https://juejin.im/post/5b8d46c3e51d4538e710bc78