做者簡介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仍是挺方便的。
混合開發中遇到的另一個問題就是頁面的跳轉管理問題,尤爲是原生和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談一些本身的體會。
咱們的佈局組合大部分須要繼承這兩個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這種邏輯了。
這實際上是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則儘可能減小嵌套。
三、多child。這類Widget對應咱們在Android開發中的ViewGroup,涉及到頁面的佈局展現。
在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框架。
閱讀博客還不過癮?
歡迎你們掃二維碼經過添加羣助手,加入交流羣,討論和博客有關的技術問題,還能夠和博主有更多互動
博客轉載、線下活動及合做等問題請郵件至 shadowfly_zyl@hotmail.com 進行溝通 ![]()