學習Flutter也有一陣子了。閒著沒事,用了公司一個已經涼涼的App設計圖來練手。當然了介面不可能用的了,所以都是些死資料,實現效果可以說是很完美了(得到了設計的認可。。。)。當然自己也是邊查邊寫,也借鑑了許多Github上優秀的Flutter專案。現在開源出來(附帶設計圖),供大家交流學習。希望多多Star、Fork支援,有問題可以Issue。附上鍊接:github.com/simplezhli/…
本篇主要分享一下自己在此專案中遇到的問題及心得,希望對你有所幫助!
1.部件溢位
異常大致如下:
A RenderFlex overflowed by 22 pixels on the bottom.
複製程式碼
導致的原因就是在水平或者垂直方向上的內容超過了父部件的大小。一般來說我們的頁面不存在這樣的問題,因為根據頁面的設計,事先可以預料到是否超出。不過要注意到有輸入法彈出的頁面。比如我下面的這個例子:
可以看到底部溢位了22個畫素,可能在18:9的手機以上不太會出現這種問題,因為螢幕的高度足夠。但是這種16:9的手機可能會暴露出來。解決的方法有兩種:
-
包一層
SingleChildScrollView
,讓你的頁面可以滑動起來。 -
在
Scaffold
中設定resizeToAvoidBottomInset
為false。預設為ture,防止部件被遮擋。如果使用了這個方法,如果底部有輸入框,則會造成遮擋。
大家可以根據實際需求選擇。
2.輸入框的遮擋
頁面如下:
底部有輸入框,同時“提交”的按鈕固定在底部。一開始覺得既然固定在底部,那就使用Stack
配合Positioned
來實現,然而就導致輸入法彈出時,發生遮擋。
上圖中,我選中了最後一個輸入框,但因為輸入法預設都是在輸入框的下方彈出,然而上面蓋著這個“提交”按鈕,發生了遮擋。
最終我的解決方法就是使用Column
配合Expanded
來實現。修復後如下:
3.SafeArea
一旦有部件固定在頂部或者底部(嚴謹點的話可以說是在螢幕的四邊)。那我我們最好使用SafeArea
來包一下。因為Android 和 IOS都有狀態列,甚至IOS還有叫做“HomeIndicator”的橫條。所以一不留神就會出現適配問題。
我們在Flutter中常使用的BottomNavigationBar
和 AppBar
其實就在內部處理了此類問題。以 AppBar
原始碼為例:
class _AppBarState extends State<AppBar> {
@override
Widget build(BuildContext context) {
if (widget.primary) {
appBar = SafeArea( // <--- 1
top: true,
child: appBar,
);
}
return Semantics(
container: true,
child: AnnotatedRegion<SystemUiOverlayStyle>(
value: overlayStyle,
child: Material( // <--- 2
color: widget.backgroundColor
?? appBarTheme.color
?? themeData.primaryColor,
child: Semantics(
explicitChildNodes: true,
child: appBar,
),
),
),
);
}
}
複製程式碼
所以使用方法為:
Material( // 需要顏色填充到邊界區域可以使用
color: Colors.white,
child: SafeArea(
child: Container(),
),
)
複製程式碼
還是上面的頁面,我們對比一下處理前後的效果:
4.善用Theme
Flutter 在開發中,讓人詬病的就是大量的巢狀,而我們只能儘量避免。比如將一些部件、屬性進行封裝,避免重複的書寫。不過封裝也講究使用場景。如果這種樣式的部件僅僅只是某一兩處使用,封裝顯得有點小題大做。並且封裝的大而全也會增加使用的複雜度。那麼這時就可以使用Theme這種辦法。
舉一個例子,在下圖中圈起來的部分有三個按鈕,它們的高度相同,文字、圓角大小也相同。如果每一個都去設定這些屬性,未免太過麻煩。
這時我們使用Theme去統一修改它們的樣式,就會很方便了。
Theme(
data: Theme.of(context).copyWith(
buttonTheme: ButtonThemeData(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
minWidth: 64.0,
height: 30.0,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
shape:RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4.0),
)
),
textTheme: TextTheme(
button: TextStyle(
fontSize: 14.0,
)
)
),
child: Row(
children: <Widget>[
FlatButton(
color: Color(0xFFF6F6F6),
onPressed: (){},
child: Text("聯絡客戶"),
),
......
FlatButton(
color: Color(0xFFF6F6F6),
onPressed: (){},
child: Text("拒單"),
)
],
),
)
複製程式碼
同時使用Theme
還可以修改許多預設的設定,比如FlatButton
的預設寬度為88,高度為36,但是FlatButton
中沒有直接修改的屬性,網上好多的方法都是通過包一層Container
去修改,不僅增加的巢狀,有些需求還不能達到。所以善用Theme
可以讓你省時省力,不過缺點就是你需要去翻翻原始碼,尋找使用這些Theme的地方。
5.注意平臺差異
注意部分元件在Android與IOS平臺之間的差異。
Scaffold
的AppBar
,AppBar
中預設的title
在Android中靠左顯示,IOS中居中顯示。如果需要兩個平臺效果統一,需要設定在AppBar
中主動設定centerTitle
屬性。同時AppBar
的返回箭頭圖示也不相同,統一的話需要自定義leading
。
- 頁面跳轉如果使用
MaterialPageRoute
來做過渡效果,注意Android中新的頁面會從螢幕底部滑動到螢幕頂部,IOS中新的頁面會從螢幕右側滑動到螢幕左側。
如果需要兩個平臺效果統一,我們不使用自帶效果,可以自定義一個。
Navigator.push(context, PageRouteBuilder(transitionDuration: Duration(milliseconds: 300),
pageBuilder: (context, animation, secondaryAnimation){
return new FadeTransition( //使用漸隱漸入過渡,
opacity: animation,
child: TestPage(),
);
})
);
複製程式碼
要麼修改Theme
,統一兩平臺的實現。:
class MyApp extends StatelessWidget {
static const Map<TargetPlatform, PageTransitionsBuilder> _defaultBuilders = <TargetPlatform, PageTransitionsBuilder>{
TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
TargetPlatform.iOS: FadeUpwardsPageTransitionsBuilder(),
};
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
pageTransitionsTheme: PageTransitionsTheme(
builders: _defaultBuilders
)
),
...
);
}
}
複製程式碼
-
ScrollPhysics效果,可以滑動的部件都有一個
physics
屬性。滑動到邊界時,Android平臺為邊緣陰影的效果ClampingScrollPhysics
,IOS為回彈效果BouncingScrollPhysics
。如果需要統一,可以指定physics
屬性。 -
狀態列方面,Android平臺預設是半透明的效果,IOS則是透明效果。比如Android要實現IOS的效果,可以設定狀態列為透明。不過IOS要實現Android的效果則不行。。。,難道只能自定義?有知道方法的可以分享一下。
void main(){
runApp(MyApp());
// 透明狀態列
if (Platform.isAndroid) {
SystemUiOverlayStyle systemUiOverlayStyle =
SystemUiOverlayStyle(statusBarColor: Colors.transparent);
SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle);
}
}
複製程式碼
- 輸入鍵盤
當TextField
的keyboardType
屬性設定為TextInputType.phone
或TextInputType.number
時,IOS系統彈出的數字輸入鍵盤沒有"完成"按鈕,導致輸入法無法關閉。當然了Android不存在這個問題。
比較成熟有效的方案是在鍵盤彈出的上方懸浮一個按鈕,點選可以關閉鍵盤。當然了,這種問題也有對應的庫可以解決,我使用的是flutter_keyboard_actions來解決了這個問題。因為在Android端我發現了部分輸入法的相容問題,所以只針對IOS做了處理。大家可以看一下前後對比圖,具體實現程式碼可以參考flutter_keyboard_actions
的文件和我的專案程式碼:
當然平臺差異不僅僅是這麼多,比如IOS自帶側滑返回等。具體我們可以去檢視呼叫TargetPlatform
列舉類的程式碼。
如果你覺得這樣真麻煩,我給你支個大招,修改ThemeData
的platform
,指定一個平臺。
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
platform: TargetPlatform.android
),
...
);
}
}
複製程式碼
其次就是使用TextInputType.number
在IOS中彈起的鍵盤沒有小數點符號。在輸入金額型別資料時,需要將keyboardType
屬性設定為TextInputType.numberWithOptions(decimal: true)
。
6.keyboardType
keyboardType
屬性主要含義為彈起的鍵盤型別,並不代表輸入資料的型別。
而在Android開發中,在EditText
中設定android:inputType
不僅可以指定彈起的鍵盤型別,同時也確定了輸入資料的型別,也就是內建了資料的格式校驗。Flutter中並沒有後者,所以可能一開始你是TextInputType.number
,但是在輸入法中切換成中文鍵盤,一樣可以輸入中文字元。所以資料的校驗需要我們使用inputFormatters
自己處理。
比如TextInputType.phone
時可以使用WhitelistingTextInputFormatter
白名單校驗,只允許輸入0~9:
TextField(
keyboardType: TextInputType.phone,
inputFormatters: [WhitelistingTextInputFormatter(RegExp("[0-9]"))]
)
複製程式碼
輸入密碼時可以使用BlacklistingTextInputFormatter
黑名單校驗,除去中文字元:
TextField(
keyboardType: TextInputType.text,
inputFormatters: [BlacklistingTextInputFormatter(RegExp("[\u4e00-\u9fa5]"))]
)
複製程式碼
輸入小數時,可以自定義TextInputFormatter
來限制輸入小數格式:
TextField(
keyboardType: TextInputType.numberWithOptions(decimal: true),
inputFormatters: [UsNumberTextInputFormatter()]
)
//來源:https://www.cnblogs.com/yangyxd/p/9639588.html
class UsNumberTextInputFormatter extends TextInputFormatter {
static const defaultDouble = 0.001;
static double strToFloat(String str, [double defaultValue = defaultDouble]) {
try {
return double.parse(str);
} catch (e) {
return defaultValue;
}
}
@override
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
String value = newValue.text;
int selectionIndex = newValue.selection.end;
if (value == ".") {
value = "0.";
selectionIndex++;
} else if (value != "" && value != defaultDouble.toString() && strToFloat(value, defaultDouble) == defaultDouble) {
value = oldValue.text;
selectionIndex = oldValue.selection.end;
}
return new TextEditingValue(
text: value,
selection: new TextSelection.collapsed(offset: selectionIndex),
);
}
}
複製程式碼
7.InkWell
InkWell
有的叫濺墨效果,有的叫水波紋效果。使用場景是給一些無點選事件的部件新增點選事件時使用(也支援長按、雙擊等事件),同時你也可以去修改它的顏色和形狀。
InkWell(
borderRadius: BorderRadius.circular(8.0), // 圓角
splashColor: Colors.transparent, // 濺墨色(波紋色)
highlightColor: Colors.transparent, // 點選時的背景色(高亮色)
onTap: () {},// 點選事件
child: Container(),
);
複製程式碼
不過有時你會發現並不是包一層InkWell
就一定會有濺墨效果。主要原因是濺墨效果是在一個背景效果,並不是覆蓋的前景效果。所以InkWell
中的child一旦有設定背景圖或背景色,那麼就會遮住這個濺墨效果。如果你需要這個濺墨效果,有兩種方式實現。
- 包一層
Material
,將背景色設定在Material
中的color裡。
Material(
color: Colors.white,
child: InkWell(),
)
複製程式碼
- 使用
Stack
佈局,將InkWell
放置在上層。這種適用於給圖片新增點選效果,比如Banner圖的點選。
Stack(
children: <Widget>[
Positioned.fill(
child: Image(),
),
Positioned.fill(
child: Material(
color: Colors.transparent,
child: InkWell(
splashColor: Color(0X40FFFFFF),
highlightColor: Colors.transparent,
onTap: () {},
),
),
)
],
)
複製程式碼
8.保持頁面狀態
比如點選導航欄來回切換頁面,預設情況下會丟失原頁面狀態,也就是每次切換都會重新初始化頁面。這種情況解決方法就是PageView
與BottomNavigationBar
結合使用,同時子頁面State
中繼承AutomaticKeepAliveClientMixin
並重寫wantKeepAlive
為true。程式碼大致如下:
class _TestState extends State<Test> with AutomaticKeepAliveClientMixin{
@override
Widget build(BuildContext context) {
super.build(context);
return Container();
}
@override
bool get wantKeepAlive => true;
}
複製程式碼
詳細的可以看這篇文章:Flutter 三種方式實現頁面切換後保持原頁面狀態
9.依賴版本問題
首先這裡建議凡是Flutter的外掛在填寫版本號時不要使用^
符號。
^
符號意味著你可以使用此外掛的最新版本(大於等於當前版本)。這會導致什麼問題呢?可能你前一天程式碼還能跑起來,今天就編譯出錯了。因為這些外掛中包括Android、IOS的所用依賴環境配置,常見的就是新版本使用了AndroidX的依賴,但是還有些外掛並沒有使用AndroidX,導致了兩者的衝突。
我之前在看flutter-go
的程式碼時,就是因為webview的外掛突然升級了,導致了安裝失敗。具體問題可以看這裡。所以在程式碼穩定的情況下不建議使用^
符號。
發生了這種問題,有以下幾個解決方法:
-
使用非AndroidX的版本外掛。(優點就是見效快。缺點就是此外掛後續的更新無法使用)
-
手動修改外掛的衝突,因為Flutter外掛的程式碼是可以直接修改的,所以你可以手動修改掉這些衝突,統一外掛的版本(優點就是可以使用最新的版本。缺點就是這種方法首先麻煩,其次不利於團隊開發使用)
我偏好使用第二種,只要做好修改的相關記錄就行,算是一勞永逸。
10.Flutter Android 打包
打包本身流程沒有問題,配置好籤名檔案,執行flutter build apk
命令。但是發現打包後沒有將外掛中的AndroidManifest.xml
檔案合併。比如我有使用image_picker
外掛,它的AndroidManifest.xml
檔案如下:
可以看到有許可權的及Android 7.0FileProvider
的宣告。諸如此類的資訊沒有打包進去(但是引用xml中的flutter_image_picker_file_paths
檔案卻在),導致我實際使用這些功能時沒有反應,但是在平時的除錯過程中卻是好的。
中間我發現打包後的App名稱也是之前的,懷疑是快取問題,所以我手動刪除了專案根目錄的build
與.gradle
資料夾,重新打包就好了。所以打包後最好檢查一下AndroidManifest.xml
檔案,避免此類快取造成的問題。
11.其他
-
Container
功能強大,設定寬高、padding、margin、背景色、背景圖、圓角、陰影等都可以使用它。 -
有些
widget
自帶padding
屬性,所以不必多套一層Padding
部件。(比如ListView
、GridView
、Container
、ScrollView
、Button
) -
儘量使用
const
來定義常量。比如padding
、color
、style
這些地方:
class Colours {
static const Color text_dark = Color(0xFF333333);
}
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
"Test",
style: TextStyle(
fontSize: 26.0,
color: Colours.text_dark
)
)
)
複製程式碼
- Dart2中的
new
關鍵字可選,所以就不要選了,哈哈!!
其實我在這中間遇到的小問題還有很多,有的暫時還沒有找到好的方法去解決。不過這才剛剛開始,希望Flutter越來越好。
篇幅有限,那麼先分享以上11條Tips,如果本篇對你有所幫助,可以點贊支援!最後再次奉上Github地址:github.com/simplezhli/…