阅读 7310

与Flutter第一次亲密接触-Android 视角

作者简介

万坤,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的目录中,系统帮我们罗列了大概有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中。



阅读博客还不过瘾?

欢迎大家扫二维码通过添加群助手,加入交流群,讨论和博客有关的技术问题,还可以和博主有更多互动

博客转载、线下活动及合作等问题请邮件至 shadowfly_zyl@hotmail.com 进行沟通

关注下面的标签,发现更多相似文章
评论