與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 進行溝通
相關文章
相關標籤/搜索