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

做者简介java

万坤,5年安卓开发经验,16年加入饿了么,现任职饿了么资深安卓开发工程师,负责饿了么物流安卓相关APP线上的高稳定运行。android

前言

Flutter在今年6月份发布第一个Release预览版以来,开发热度呈现了井喷式的爆发。Github上Flutter项目的小星星也已经涨到了3.6万了,同时国内闲鱼团队已经将Flutter用到了业务中并上线运行。能够说Flutter已经有了很是成熟的使用环境,在咱们团队内部你们也是跃跃欲试。这里我选择了咱们团队页面中一个比较轻量级的页面-设置页面,完成了 Flutter的开发和上线准备工做,下面主要是分享一下这一次亲密接触的经验和心得。shell

混合开发

实际上咱们若是想把Flutter引入到现有的业务中去,就必然会涉及到Flutter和Native混合开发的问题,尤为是Flutter的代码怎么引入到咱们原有的工程(实际上官方的Demo是一个纯Flutter的工程)。我这边参考闲鱼的作法,在Android端实现的主要步骤以下:缓存

  • 1.新建一个Android的module工程。将此工程做为Flutter相关业务打包的工程,最终输出aar供主工程直接依赖;bash

  • 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。app

  • 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代码修改后直接接入到工程中运行:less

    ./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进行分类,大概能够分红这几类:

  • 一、无child。这类Widget对应咱们在Android开发中的基础View,基本上是页面展现的最基础的元素了,像Text、Image、Icon、Checkbox、Switch等,使用比较简单,这里就不详细讲述了。

  • 二、单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。设定子控件的对齐方式。
  • 三、多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 进行沟通
相关文章
相关标签/搜索