作為系列文章的第三篇,繼篇章一和篇章二之後,本篇將為你著重展示:Flutter開發過程的打包流程、APP包對比、細節技巧與問題處理。本篇主要描述的Flutter的打包、在開發過程中遇到的各類問題與細節,算是對上兩篇的補全。
友情提示:本文所有程式碼均在 GSYGithubAppFlutter ,要不試試?(◐‿◑)。
一、打包
首先我們先看結果,如下表所示,是 Flutter 與 React Native 、IOS 與 Android 的縱向與橫向對比 。
專案 | IOS | Android |
---|---|---|
GSYGithubAppFlutter | ||
GSYGithubAppRN |
從上表我們可以看到:
-
Fluuter的 apk 會比 ipa 更小一些,這其中的一部分原因是 Flutter 使用的
Skia
在Android 上是自帶的。 -
橫向對比 React Native ,雖然專案不完全一樣,但是大部分功能一致的情況下, Flutter 的 Apk 確實更小一些。這裡又有一個細節,rn 的 ipa 包體積小很多,這其實是因為
javascriptcore
在 ios上 是內建的原因。 -
對上述內容有興趣的可以看看《移動端跨平臺開發的深度解析》。
1、Android打包
在Android的打包上,筆者基本沒有遇到什麼問題,在android/app/build.grade
檔案下,配置applicationId
、versionCode
、versionName
和簽名資訊,最後通過 flutter build app
即可完成編譯。程式設計成功的包在 build/app/outputs/apk/release
下。
2、IOS打包與真機執行
在IOS的打包上,筆者倒是經歷了一波曲折,這裡主要講筆者遇到的問題。
首先你需要一個 apple 開發者賬號,然後建立證照、建立AppId,建立配置檔案、最後在info.plist
檔案下輸入相關資訊,更詳細可看官方的《釋出的IOS版APP》的教程。
但由於筆者專案中使用了第三方的外掛包如 shared_preferences
等,在執行 Archive
的過程卻一直出現如下問題:
在 `Archive` 時提示找不到
#import <connectivity/ConnectivityPlugin.h> ///file not found
#import <device_info/DeviceInfoPlugin.h>
#import <flutter_statusbar/FlutterStatusbarPlugin.h>
#import <flutter_webview_plugin/FlutterWebviewPlugin.h>
#import <fluttertoast/FluttertoastPlugin.h>
#import <get_version/GetVersionPlugin.h>
#import <package_info/PackageInfoPlugin.h>
#import <share/SharePlugin.h>
#import <shared_preferences/SharedPreferencesPlugin.h>
#import <sqflite/SqflitePlugin.h>
#import <url_launcher/UrlLauncherPlugin.h>
複製程式碼
通過 Android Studio 執行到 IOS 模擬器時沒有任何問題,說明這不是第三方包問題。通過查詢問題發現,在 IOS 執行 Archive
之前,需要執行 flutter build release
,如下圖在命令執行之後,Pod 的執行目錄會發現改變,並且生成打包需要的檔案。(ps 普通執行時自動又會修改回來)
但是實際在執行 flutter build release
後,問題依然存在,最終翻山越嶺(╯‵□′)╯︵┻━┻,終於找到兩個答案:
-
Issue#19241 下描述了類似問題,但是他們因為路徑問題導致,經過嘗試並不能解決。
-
Issue#18305 真實的解決了這個問題,居然是因為 Pod 的工程沒引入:
open ios/Runner.xcodeproj
I checked Runner/Pods is empty in Xcode sidebar.
drop Pods/Pods.xcodeproj into Runner/Pods.
"Valid architectures" to only "arm64" (I removed armv7 armv7s)
複製程式碼
最後終於成功打包,心累啊(///▽///)。同時如果希望直接在真機上除錯 Flutter,可以參考 :《Flutter基礎—開發環境與入門》 下的 IOS 真機部分。
二、細節
這裡主要講一些小細節
1、AppBar
在 Flutter 中 AppBar 算是常用 Widget ,而 AppBar 可不僅僅作為標題欄和使用,AppBar上的 leading
和 bottom
同樣是有用的功能。
- AppBar 的
bottom
預設支援TabBar
, 也就是常見的頂部 Tab 的效果,這其實是因為TabBar
實現了PreferredSizeWidget
的preferredSize
。 所以只要你的控制元件實現了preferredSize
,就可以放到 AppBar 的bottom
中使用。比如下圖搜尋欄,這是TabView下的頁面又實用了AppBar。
-
leading
:通常是左側按鍵,不設定時一般是 Drawer 的圖示或者返回按鈕。 -
flexibleSpace
:位於bottom
和leading
之間。
2、按鍵
Flutter 中的按鍵,如 FlatButton
預設是否有邊距和最小大小的。所以如果你想要無 padding、margin、border 、預設大小 等的按鍵效果,其中一種方式如下:
///
new RawMaterialButton(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
padding: padding ?? const EdgeInsets.all(0.0),
constraints: const BoxConstraints(minWidth: 0.0, minHeight: 0.0),
child: child,
onPressed: onPressed);
複製程式碼
如果在再上 Flex ,如下所示,一個可控的填充按鍵就出來了。
new RawMaterialButton(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
padding: padding ?? const EdgeInsets.all(0.0),
constraints: const BoxConstraints(minWidth: 0.0, minHeight: 0.0),
///flex
child: new Flex(
mainAxisAlignment: mainAxisAlignment,
direction: Axis.horizontal,
children: <Widget>[],
),
onPressed: onPressed);
複製程式碼
3、StatefulWidget 賦值
這裡我們以給 TextField
主動賦值為例,其實 Flutter 中,給有狀態的 Widget 傳遞狀態或者資料,一般都是通過各種 controller 。如 TextField
的主動賦值,如下程式碼所示:
final TextEditingController controller = new TextEditingController();
@override
void didChangeDependencies() {
super.didChangeDependencies();
///通過給 controller 的 value 新建立一個 TextEditingValue
controller.value = new TextEditingValue(text: "給輸入框填入引數");
}
@override
Widget build(BuildContext context) {
return new TextField(
///controller
controller: controller,
onChanged: onChanged,
obscureText: obscureText,
decoration: new InputDecoration(
hintText: hintText,
icon: iconData == null ? null : new Icon(iconData),
),
);
}
複製程式碼
其實 TextEditingValue
是 ValueNotifier
,其中 value
的 setter 方法被過載,一旦改變就會觸發 notifyListeners
方法。而 TextEditingController
中,通過呼叫 addListener
就監聽了資料的改變,從而讓UI更新。
當然,賦值有更簡單粗暴的做法是:傳遞一個物件 class A 物件,在控制元件內部使用物件 A.b 的變數繫結控制元件,外部通過 setState({ A.b = b2}) 更新。
4、GlobalKey
在Flutter中,要主動改變子控制元件的狀態,還可以使用 GlobalKey
。 比如你需要主動呼叫 RefreshIndicator
顯示重新整理狀態,如下程式碼所示。
GlobalKey<RefreshIndicatorState> refreshIndicatorKey;
showForRefresh() {
///顯示重新整理
refreshIndicatorKey.currentState.show();
}
@override
Widget build(BuildContext context) {
refreshIndicatorKey = new GlobalKey<RefreshIndicatorState>();
return new RefreshIndicator(
key: refreshIndicatorKey,
onRefresh: onRefresh,
child: new ListView.builder(
///·····
),
);
}
複製程式碼
5、Redux 與主題
使用 Redux 來做 Flutter 的全域性 State 管理最合適不過,由於Redux內容較多,如果感興趣的可以看看 篇章二 ,這裡主要通過 Redux 來實現實時切換主題的效果。
如下程式碼,通過 StoreProvider
載入了 store ,再通過 StoreBuilder
將 store 中的 themeData 繫結到 MaterialApp
的 theme 下,之後在其他 Widget 中通過 Theme.of(context)
調你需要的顏色,最終在任意位置呼叫 store.dispatch
就可實時修改主題,效果如後圖所示。
class FlutterReduxApp extends StatelessWidget {
final store = new Store<GSYState>(
appReducer,
initialState: new GSYState(
themeData: new ThemeData(
primarySwatch: GSYColors.primarySwatch,
),
),
);
FlutterReduxApp({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
/// 通過 StoreProvider 應用 store
return new StoreProvider(
store: store,
///通過 StoreBuilder 獲取 themeData
child: new StoreBuilder<GSYState>(builder: (context, store) {
return new MaterialApp(
theme: store.state.themeData,
routes: {
HomePage.sName: (context) {
return HomePage();
},
});
}),
);
}
}
複製程式碼
6、Hotload 與 Package
Flutter 在 Debug 和 Release 下分別是 JIT 和 AOT 模式,而在 DEBUG 下,是支援 Hotload 的,而且十分絲滑。但是需要注意的是:如果開發過程中安裝了新的第三方包 ,而新的第三方包如果包含了原生程式碼,需要停止後重新執行哦。
pubspec.yaml
檔案下就是我們的包依賴目錄,其中 ^
代表大於等於,一般情況下 upgrade
和 get
都能達到下載包的作用。但是:upgrade 會在包有更新的情況下,更新 pubspec.lock
檔案下包的版本 。
三、問題處理
Waiting for another flutter command to release the startup lock
:如果遇到這個問題:
1、開啟flutter的安裝目錄/bin/cache/
2、刪除lockfile檔案
3、重啟AndroidStudio
複製程式碼
-
dialog下的黃色線 yellow-lines-under-text-widgets-in-flutter:showDialog 中,預設是沒使用 Scaffold ,這回導致文字有黃色溢位線提示,可以使用 Material 包一層處理。
-
TabBar + TabView + KeepAlive 的問題 可以通過 TabBar + PageView 解決,具體可見 篇章二。
自此,第三篇終於結束了!(///▽///)
資源推薦
- Github : github.com/CarGuo
- 本文程式碼 :github.com/CarGuo/GSYG…