接觸 Flutter 已經有一陣子了,期間記錄了很多開發小問題,苦於忙碌沒時間整理,最近專案進度步上正軌,藉此機會抽出點時間來統一記錄這些問題,並分享專案開發中的一點心得以及多平臺打包的一些注意事項,希望能對大家有所幫助?。
UI 元件使用
官方為我們提供了大量原生效果的元件,如以 Android 中常見的 Material Design 系列元件和 iOS 系統中讓設計師們“欲罷不能”的 Cupertino 系列元件。從我這一個月左右對於 Flutter UI 元件的使用情況來看,不得不感慨一句:“真香”。由於本人之前是做 Android 開發的,所以對於 Android 方面的一些“詬病”深有體會。例如,設計師經常讓我們還原設計稿中的陰影效果,一般需要設定陰影顏色、x/y偏移量和模糊度等,然而 Android 原生並沒有提供支援所有這些屬性的一款元件,所以只能我們自己通過自定義控制元件去實現,現在還有多少人依然通過 CardView 來“魚目混珠”呢?然而,在 Flutter 中就無需擔心這種問題,通過類似前端中常用的盒子元件—— Container 就可以輕鬆實現。
當然,Flutter 雖然很強大,但 UI 元件也不是萬能的,跨平臺之路註定漫長而佈滿荊棘,偶爾也會伴隨著一些小問題。
TextField
-
軟鍵盤彈起後元件溢位的問題
由於頁面不支援滾動,一旦使用 TextField,軟鍵盤彈起後很容易會覆蓋一些UI元件,如果不以為意,那麼下面這個問題就會成為“家常便飯”:
A RenderFlex overflowed by xx pixels on the bottom. 複製程式碼
常用的解決方案就是通過巢狀一層
SingleChildScrollView
來規避,當軟鍵盤彈起時,下方的元件會被軟鍵盤自動頂上去。 -
HintText 不居中問題
這個問題很多人應該都遇到過,當我們在專案中設定中文 Locale 後,在 TextField 的 InputDecoration 中設定
hintText
時,會發現提示文字向下偏移幾個畫素,這應該屬於 Flutter 的bug。如何解決這個問題呢?很簡單,只需要設定textBaseine
屬性,如下程式碼所示:TextFormField( decoration: InputDecoration( prefixIcon: Icon( Icons.lock_outline ), hintText: S.of(context).loginPasswordHint, ), style: TextStyle( /// handle hint text offset problem. textBaseline: TextBaseline.alphabetic ), keyboardType: TextInputType.number, onSaved: (password) {}, ) 複製程式碼
具體可參考:github.com/flutter/flu…
-
焦點問題
輸入框的焦點問題主要體現在兩點:
- 前往另一個頁面返回後自動彈出了軟鍵盤(即自動獲取了焦點)
- iOS手機上切換至數字鍵盤後無法關閉軟鍵盤
這兩個問題其實都可以藉助
FocusNode
來解決,先來看下面一段程式碼:FocusNode _writingFocusNode = FocusNode(); ... void _clearTextFieldFocus() { if (_writingFocusNode.hasFocus) { _writingFocusNode.unfocus(); } } 複製程式碼
上述程式碼建立了一個
FocusNode
物件,並宣告瞭移除焦點的方法,相信大家不難判斷出。此外,我們需要給TextField
的focusNode
屬性傳入我們建立的_writingFocusNode
。問題一中,我們可以在頁面跳轉前先移除焦點,這樣,從二級頁面返回後輸入框就不會自動彈出軟鍵盤。問題二中,我們可以在使用者點選空白區域後自動移除焦點(關閉軟鍵盤),以下程式碼供參考:Widget _buildInputArea() => Stack( children: <Widget>[ // 通過空白區域的點選事件來關閉軟鍵盤 GestureDetector( onTap: () { _clearTextFieldFocus(); }, child: Container( /// 此處注意設定背景顏色,否則預設透明色可能會穿透,無法響應點選事件 color: AppTheme.surfaceColor, width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height, ), ), Column( children: <Widget>[ ScreenUtils.verticalSpace(32), // account input edit text Padding( padding: EdgeInsets.only(bottom: AutoSize.covert.dpToDp(12)), child: TextField( controller: _accountTextController, decoration: InputDecoration( prefixIcon: Padding( padding: EdgeInsets.all(AutoSize.covert.dpToDp(12)), child: ImageIcon(AssetImage(ImageAssets.ic_login_user_input)), ), hintText: S.of(context).loginAccountHint, ), keyboardType: TextInputType.number, ), ), // password input edit text Padding( padding: EdgeInsets.only(bottom: AutoSize.covert.dpToDp(12)), child: ValueListenableBuilder( valueListenable: obscureTextModel, builder: (context, value, child) => TextField( controller: _passwordTextController, obscureText: value, decoration: InputDecoration( prefixIcon: Padding( padding: EdgeInsets.all(AutoSize.covert.dpToDp(12)), child: ImageIcon(AssetImage(ImageAssets.ic_login_pwd_input)), ), suffixIcon: IconButton( icon: Icon(value ? Icons.visibility_off : Icons.visibility, size: AutoSize.covert.dpToDp(20)), onPressed: () { obscureTextModel.value = !value; } ), hintText: S.of(context).loginPasswordHint, ), keyboardType: TextInputType.text, ), ), ), ], ), ], ); 複製程式碼
Container
-
盒子模型特點
對於接觸過前端的人來說,應該都領略過“盒子模型”的強大了,所以,Container 的強大之處相信也不用我多說了:它幾乎是一個萬能的“容器”,既能設定 margin、padding、aligment,又可以裝飾它的背景 docoration 屬性,例如陰影效果、漸變色、圓角效果等等。
-
設定背景色問題
Container雖好,但也需要在使用時注意一些問題,例如,它的原始碼註釋中就說到:我們可以通過
color
和decoration
來設定盒子背景,但兩者卻不能同時存在,如果我們既希望保留背景色,又想使用裝飾器 (decoration),我們可以直接設定BoxDecoration
的color
屬性。
SafeArea
Android中存在狀態列、底部導航欄,而 iOS 中也存在狀態列和"底部導航條",所以如果我們頁面中的邊界部分需要固定顯示一些小元件,那麼我們最好能夠在最外層巢狀一層 SafeArea
元件,即讓UI元件處於“安全區域”,不至於引起適配問題。
Material(
color: AppTheme.surfaceColor,
child: SafeArea(
child: Container(),
),
)
複製程式碼
列表元件
Flutter中常見的列表元件有 ListView、GridView、PageView 等,一個完整的應用肯定也離不開這些元件。我們在使用時,需要留意以下幾點:
-
Vertical viewport was given unbounded height 問題
作為初學者,我們在初期應該都碰到過這個問題:Vertical/Horizontal viewport was given unbounded height,這是由於我們沒有給列表元件指定高度或者寬度而引起的,一般出現場景是在
Column
中,我們可以給列表元件包裹一層盒子或者被Expanded
包裹,讓其儘可能佔據最大空間:Column( children:[ ..., Expanded( child: GridView.builder( .... ) ) ) ] ) 複製程式碼
-
physics 屬性
做過原生開發的都知道,在 Android 中,支援滾動的元件滑到頂或者滑到底後自帶 colorPrimary 色調的水波紋效果,至於iOS,則是越界回彈效果。你拿著 Android 中的預設效果去給設計師看,“親果黨”的他們肯定不幹了,硬是讓你改成 iOS 的回彈效果。所幸,Flutter 早就考慮到了這一點,支援滑動的元件中都提供了
physics
屬性,只需要將其設定為BouncingScrollPhysics
就能完美實現回彈效果。同時,physics 還有其他屬性,這裡不再一一介紹,大家可以去檢視相關文件和原始碼,這裡提一下NeverScrollableScrollPhysics
,即禁止滑動。這個有什麼用呢?其實還是挺有用的,比如巢狀的兩個滑動元件中就可以將其中一個的 physics 屬性設定為NeverScrollableScrollPhysics
,這樣可以簡單快速解決滑動衝突。此外,有些特殊場景我們可能不希望使用者可以滑動,而是通過按鈕點選來控制列表滑動,這時候,設定該屬性就再好不過啦。
自定義彈窗
Flutter 為我們提供了一些內建的定製彈窗,這裡不再一一說明了。如何自定義彈窗?其實很簡單,只需要明白:彈窗即頁面。以下面的效果為例:
相信對於大家來說,上面的UI頁面實現起來並不困難,那我們離 Dialog 效果僅剩一步之遙了:點選空白區域關閉。其實,在上面的某段程式碼中我已經貼了關鍵程式碼,細心的小夥伴應該也察覺到了,沒錯,我們可以通過 Stack
元件包裹半透明蒙層(如Container)和分享功能元件,我們只需為半透明蒙層增加點選事件即可:
Stack(
children: <Widget>[
// 通過空白區域的點選事件來關閉彈窗
GestureDetector(
onTap: () {
//關閉彈窗
Navigator.maybePop(context);
},
child: Container(
color: AppTheme.dialogBackgroundColor,
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
),
),
Container(
child: ...
)
)
複製程式碼
哈哈,是不是有種恍然大悟的感覺,如此一來,彈窗對於我們來說不就是寫一個頁面那麼簡單了嗎?。
InkWell
InkWell 在 Android 中比較常見,俗稱“水波紋”效果,屬於按鈕的一種,它支援設定波紋顏色、圓角等屬性。我們偶爾可能會遇到水波紋失效的問題,這一般是因為我們在 InkWell
內部的 child 中設定了背景,從而導致水波紋效果被遮蓋。如何解決這個問題?其實很簡單,只需要在 InkWell
外層套上 Material
並設定 color
即可:
Material(
color: Colors.white,
child: InkWell(
borderRadius: AppTheme.buttonRadius, // 圓角
splashColor: AppTheme.splashColor, // 波紋顏色
highlightColor: Colors.transparent, // 點選狀態
onTap: () {}, // 點選事件
child: Container(
...
),
),
)
複製程式碼
或者,我們也可以藉助於之前實現自定義 Dialog 的思路,使用 Stack
包裹需要點選的區域,並將 InkWell
放在上層:
Stack(
children: <Widget>[
Image(),
Material(
color: Colors.transparent,
child: InkWell(
splashColor: AppTheme.splashColor,
onTap: () {},
),
)
)
],
)
複製程式碼
以上僅列舉了部分常見UI元件的使用技巧和問題,如有其他問題歡迎留言探討。
功能需求實現
除了 Flutter 中的一些 UI 元件的的使用以外,應用自然還需要涉及到很多具體的業務功能需求,常見的有第三方登入、分享、地圖、Lottie 動畫接入、第三方字型下載和載入等等。這個時候就需要我們靈活變通了,在保證專案進度順利進行的前提下有選擇性地去借助一些外掛和工具,或者前往 Flutter 的 Github Issue 社群去尋找答案了,這裡也選擇幾個常用需求簡單說一下。
當前裝置的系統語言
很多時候我們需要根據當前系統使用的語言去動態選擇載入的內容,舉個例子,我們經常需要根據當前語言去載入中文或者英文版的使用者隱私條款,我們可以藉助 Localizations 去獲取當前使用語言的 languageCode
,進而比對和處理:
/// 判斷當前語言型別
_navigateToUrl(Localizations.localeOf(context).languageCode == 'zh'
? Api.PRIVACY_POLICY_ZH_CN
: Api.PRIVACY_POLICY_EN);
複製程式碼
第三方登入/分享
這部分當初考慮自己寫外掛來對接原生的分享sdk,但考慮到時間成本就暫時擱置了,找到幾個不錯的外掛來實現了該部分功能:
-
fluwx
該外掛應該是在微信社會化分享、支付等方面功能整合度比較高的外掛了,是由 OpenFlutter 社群負責維護的,目前沒發現有什麼問題。具體配置的細節就不再說明了,它的文件很詳細,具體可參考:github.com/OpenFlutter…
另外,他們組織中還有其他很多優秀的 Flutter 相關專案,大家也可以去學習一下。
-
flutter fake toolkit
這個是一系列外掛,包括了微信、微博、QQ、支付寶等眾多平臺,讓人佩服作者的產出率。目前使用起來也沒發現什麼大問題,也希望作者能夠多邀請幾個小夥伴來維護,提升更新的頻率。這裡附上其中幾個常用的外掛:
Lottie動畫
相信大家對 Airbnb 公司推出的這個動畫工具已經有所耳聞了,Lottie 支援多平臺,使用同一個JSON 動畫檔案,可在不同平臺實現相同的動畫效果。現在複雜動畫很多時候都藉助於它,能夠有效減少開發成本和保持動畫的高還原度。同樣,Flutter 中也有一些封裝了 Lottie 動畫的外掛,讓我們可以在 Flutter 上也可以感受到它的魅力。
這裡,我個人使用的外掛是 flutter_lottie
外掛,還算穩定,支援動畫屬性和進度操作,唯一遺憾就是有段時間沒更新了?,後續考慮到 iOS 方面的相容性可能會自己寫一個外掛。在 pubspec.yaml
中依賴操作如下:
# Use Lottie animation in Flutter.
# @link: https://pub.dev/packages/flutter_lottie
flutter_lottie: 0.2.0
複製程式碼
具體使用技巧可參考它的example:github.com/CameronStua…
這裡附上控制動畫進度的部分程式碼:
int _currentIndex = 0;
LottieController _lottieController;
PageController _pageController = PageController();
// the key frames of animation
final ANIMATION_PROGRESS = [
0.0,
0.2083,
0.594,
0.8333,
1
];
// the duration of each animation sections
final ANIMATION_TIMES = [
2300,
4500,
3500
];
// animation progress controller
Animation<double> animation;
AnimationController _animationController;
@override
void initState() {
super.initState();
_animationController = new AnimationController(
duration: Duration(milliseconds: ANIMATION_TIMES[_currentIndex]), vsync: this);
final Animation curve =
new CurvedAnimation(parent: _animationController, curve: Curves.linear);
animation = new Tween(begin: 0.0, end: 1.0).animate(curve);
animation.addListener(() {
_applyAnimation(animation.value);
});
}
// 佈局程式碼
.......
Positioned(
bottom: 0,
child: Container(
width: MediaQuery.of(context).size.width,
// 此處為了將動畫元件居下放置
height: AutoSize.covert.dpToDp(667),
child: LottieView.fromFile(
filePath: 'assets/anims/user_guide_anim.json',
autoPlay: false,
loop: true,
reverse: true,
onViewCreated: (controller) {
_lottieController = controller;
Future.delayed(Duration(milliseconds: 1), () {
_animationController.forward();
});
},
),
),
),
// description page view
Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
margin: EdgeInsets.only(bottom: 60),
child: PageView(
physics: BouncingScrollPhysics(),
controller: _pageController,
onPageChanged: (index) {
setState(() {
_currentIndex = index;
_animationController.duration = Duration(milliseconds: ANIMATION_TIMES[index]);
});
Future.delayed(Duration(microseconds: 600), () {
_animationController.forward(from: 0);
});
},
children: _buildPageGroup(),
),
),
......
void _applyAnimation(double value) {
var startProgress = ANIMATION_PROGRESS[_currentIndex];
var endProgress = ANIMATION_PROGRESS[_currentIndex + 1];
var progress = startProgress + (endProgress - startProgress) * value;
_lottieController.setAnimationProgress(progress);
}
複製程式碼
簡單解釋一下上述程式碼邏輯,我們這裡主要藉助於 Lottie 來實現使用者引導頁的切換動畫,引導頁分為三個畫面,所以需要我們記錄和儲存動畫的關鍵幀和每段畫面的執行時間。至於動畫的控制執行權交由上層的 PageView
來滑動實現,每次滑動通過 AnimationController
和 setState((){})
來控制和重新整理每段動畫的執行時間和執行刻度。具體demo效果如下所示:
外部字型下載和載入
如果接觸過文字編輯功能開發的小夥伴應該都知道,我們一般會提供幾十種字型供使用者使用,當然,我們不可能在專案打包時就放入這麼多字型包,這樣顯而會嚴重增加安裝包大小。我們一般的做法是:當使用者第一次點選想使用某個字型時,我們會先將其下載到手機本地儲存,然後載入字型,後續當使用者再次選擇該字型,那麼直接從本地載入即可。那麼問題來了,Flutter 目前的示例中僅為我們提供了從本地 Asset 目錄下載入字型的方式,顯然想要實現上述需求,需要我們自己尋求出路。
幸運的是,上帝為我們關上了一扇門,也為我們開啟了一扇窗,Flutter 中為我們提供了一個 FontLoader 工具,它有一個 addFont
方法,支援將 ByteData
格式資料轉化為字型包並載入到應用字型資源庫:
/// Registers a font asset to be loaded by this font loader.
///
/// The [bytes] argument specifies the actual font asset bytes. Currently,
/// only TrueType (TTF) fonts are supported.
void addFont(Future<ByteData> bytes) {
if (_loaded)
throw StateError('FontLoader is already loaded');
_fontFutures.add(bytes.then(
(ByteData data) => Uint8List.view(data.buffer, data.offsetInBytes, data.lengthInBytes)
));
}
...
/// Loads this font loader's font [family] and all of its associated assets
/// into the Flutter engine, making the font available to the current
/// application.
///
/// This method should only be called once per font loader. Attempts to
/// load fonts from the same loader more than once will cause a [StateError]
/// to be thrown.
///
/// The returned future will complete with an error if any of the font asset
/// futures yield an error.
Future<void> load() async {
if (_loaded)
throw StateError('FontLoader is already loaded');
_loaded = true;
final Iterable<Future<void>> loadFutures = _fontFutures.map(
(Future<Uint8List> f) => f.then<void>(
(Uint8List list) => loadFont(list, family)
)
);
return Future.wait(loadFutures.toList());
}
複製程式碼
如此一來,那我們解決思路也就“手到擒來”了:只需要將字型下載到本地並以檔案形式儲存,在使用時將字型檔案再轉為 ByteData
資料格式供 FontLoader 載入即可。這裡附上簡化後的部分關鍵程式碼:
/// 載入外部的字型
Future loadFontFile(LetterFont font) async {
// load font file
var fontLoader = FontLoader(font.fontName);
fontLoader.addFont(await fetchFont(font));
await fontLoader.load();
}
/// 從網路下載字型資源
Future<ByteData> fetchFont(LetterFont font) async {
final response = await https.get(
font.fontUrl);
if (response.statusCode == 200) {
// 這裡也可以做儲存到本地的邏輯處理
return ByteData.view(response.bodyBytes.buffer);
} else {
// If that call was not successful, throw an error.
throw Exception('Failed to load font');
}
}
複製程式碼
打包上架相關
打包方面也有一部分細節需要注意一下,這裡談一下 Android 和 iOS 開發環境配置和打包差異以及列舉部分常見問題,其他問題因人而異,也因版本而異,就不單獨拿出來講了。
Android方面
-
開發工具
Android studio3.6穩定版
-
程式碼編譯環境
Kotlin + AndroidX
目前Flutter建立專案預設勾選兩個選項
-
版本號配置
在
android/app/build.gradle
中配置flutterVersionCode
和flutterVersionName
。注意:如果在
pubspec.yaml
中配置了version,那麼 Flutter 具體打包的版本會實際根據pubspec.yaml
的version
來構建。 -
網路配置
目前 Android 官方不建議採用http請求格式,推薦使用 https,所以,如果專案中使用到了http格式請求,那麼需要新增網路配置。首先在
android/app/src/main/res
路徑下建立名為xml
的資料夾:然後建立名為network_security_config
的 xml 檔案,接著將如下程式碼複製進去:<?xml version="1.0" encoding="utf-8"?> <network-security-config> <base-config cleartextTrafficPermitted="true"/> </network-security-config> 複製程式碼
然後在
AndroidManifest.xml
檔案中設定networkSecurityConfig
屬性即可:<application android:name="io.flutter.app.FlutterApplication" android:label="Timeory" android:icon="@mipmap/ic_launcher" tools:replace="android:name" android:usesCleartextTraffic="true" android:networkSecurityConfig="@xml/network_security_config" tools:ignore="GoogleAppIndexingWarning"> ...... </application> 複製程式碼
-
許可權配置
一般我們專案中都會用到許可權申請,並且很多 flutter 外掛中也會要求我們去自己配置許可權,我們可能需要在
AndroidManifest.xml
檔案中新增如下常用許可權(只是樣例):<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> <uses-permission android:name="android.permission.READ_PHONE_STATE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> 複製程式碼
當然這些還是不夠的,Android6.0及以上,我們還需要在程式碼中動態申請許可權,Flutter中有很多優秀的許可權申請外掛,iOS 上面一般沒問題,Android由於碎片化比較嚴重,可能會在不同機型上出現各種奇怪問題,比如,紅米部分機型藉助於
permission_hanlder
外掛申請定位許可權可能會失敗的問題,這裡需要注意一下,留個心眼。 -
Logo 配置
Logo 需要在
android/app/src/main/res
中新增和配置,一般只需要準備hdpi
、mdpi
、xhdpi
、xxhdpi
、xxxhdpi
格式即可。另外,Android8.0 及以上需要適配圓角logo,否則在部分高版本機型上會顯示 Android 預設的機器人logo。具體可以參考該文章:blog.csdn.net/guolin_blog…
-
打包
一般情況下我們通過
flutter build apk
來打包,生成的安裝在 build/app/outputs/apk/release 目錄下,這樣打出來的包一般比較大,因為它包含了arm64-v8a
、armeabi-v7a
和x86_64
三種cpu架構的包。大家可以根據需要有選擇性的針對特定機型的cpu架構打包,執行如下命令即可:flutter build apk --target-platform android-arm,android-arm64,android-x64 --split-per-abi 複製程式碼
執行完畢後就會在
release
目錄下生成三種格式的apk包。另外,大家可以選擇一些apk體積優化的方案,具體可參考:
iOS 方面
由於本人之前做 Android 開發,沒有接觸過 iOS,所以打包到 iOS 平臺還是遇到不少問題。
-
開發工具:
Xcode11.3.1 穩定版 (打包環境) + Visual Studio Code 1.42.1 (編碼環境)
-
程式碼編譯環境:Swift + Objective-C (目前建立Flutter專案預設勾選為swift,由於專案啟動時Flutter尚未更新該配置,所以專案中部分外掛採用的是oc),希望後面逐步替換為主流的swift。
-
版本號配置:
只需要在Xcode中
Runner -> targets -> General -> Identity
配置即可。 -
網路配置
iOS 中,官方同樣約束我們使用 https 請求,如果我們需要暫時使用http格式請求來測試,可以做如下配置:
在
Runner -> targets -> General -> Info
中新增App Transport Security Settings
屬性,並在此屬性標籤內新增Allow Arbitrary Loads
子屬性,並將值設定為YES
即可。 -
Logo配置
iOS 中的 logo 配置只需要找到如下入口:
點選 ➡️ 即可進入 logo 資源目錄,預設的為 Flutter 的官方 logo,我們只需要根據具體 logo 尺寸去替換資源即可。
-
國際化語言配置
專案中如果支援國際化語言,Android 中無需額外配置,iOS 中需要在
Info.plist
中新增Localized resource can be mixed
屬性,並設定值為YES
即可,否則APP執行後可能會出現實際展示的是英文的情況。 -
打包相關
Xcode打包時切記要使用穩定版,不要使用
beta
版本,否則可能會出現下面的問題:
以上就是本人對近期 Flutter 開發過程的一點簡單總結,如果能夠幫助到您那將再好不過?。剛接觸 Flutter 不久,相關闡述可能不夠嚴謹或存在理解錯誤,如果您發現了,還請指出,感謝您的閱讀。