相信大家應該都遇過這種狀況:
Column(
children: [
Text("FOO", style: TextStyle(fontSize: 12, color: Colors.red)),
Text("BAR", style: TextStyle(fontSize: 12, color: Colors.red)),
Text("FOO", style: TextStyle(fontSize: 12, color: Colors.red)),
Text("BAR", style: TextStyle(fontSize: 12, color: Colors.red)),
],
);
聰明的你應該知道,可以把它抽出來變成一個函數:
@override
Widget build(BuildContext context) {
return Column(
children: [
_buildText("FOO"),
_buildText("BAR"),
_buildText("FOO"),
_buildText("BAR"),
],
);
}
Widget _buildText(String text) {
return Text(text, style: TextStyle(fontSize: 12, color: Colors.red));
}
但如果我們的Text再複雜一點(不用仔細看,只是示範一下即使只是Text要複雜也是可以很複雜):
Widget _buildText(String text) {
return GestureDetector(
onTap: () => print("$text TAPPED!"),
child: Container(
color: Colors.blue,
padding: EdgeInsets.all(8),
decoration: BoxDecoration(boxShadow: [
BoxShadow(
color: Colors.grey,
offset: Offset(5, 5),
blurRadius: 5,
spreadRadius: 5,
)
]),
child: Text(text,
style: TextStyle(
fontSize: 12,
color: Colors.red,
shadows: [
Shadow(
color: Colors.grey,
offset: Offset(5, 5),
blurRadius: 5,
),
],
)),
),
);
}
這時候我們可能就會想要把它獨立出來變成一個Widget了:
class AwesomeText extends StatelessWidget {
const AwesomeText({ Key key, this.text }) : super(key: key);
final String text;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => print("$text TAPPED!"),
child: Container(
....
child: Text(text, ....),
),
);
}
}
// usage
Column(
children: [
AwesomText("FOO"),
AwesomText("FOO"),
AwesomText("FOO"),
],
);
但其實如果我們真的很不想寫一個新的StatelessWidget,還是可以在global scope用functional widget的寫法...:
// awesome_text.dart
import 'package:flutter/material.dart';
Widget awesomeText(String text) => GestureDetector(
onTap: () => print("$text TAPPED!"),
child: Container(
....
child: Text(
text,
....
),
),
);
所以到底什麼時候該使用StatelessWidget?為什麼我不能全部都用functional widget就好了?它們之間的差異到底是什麼?今天我們就來試著回答這個問題。
先以我們最熟悉的Text Widget為例:
@override
Widget build(BuildContext context) {
return Column(
children: [
const Text("FOO"),
const Text("FOO"),
const Text("FOO"),
const Text("FOO"),
],
);
}
若我們在建立Text物件時將其宣告為const,之後不論build被重複呼叫多少次,都不會重新建立Text物件,而是會使用同一個Text實例。而且沒有建立自然也就不用回收,雖然說單一Widget本身的建立和回收非常便宜,但若是我們有個相當複雜的Widget tree,並且可能要每秒重build 60次(例如動畫中),還是應該盡可能的把可以const的地方都設定成const。
因此當我們想從widget tree拆出某一塊:
Widget myText(String text) => const Text(text); // compile error
class MyText extends StatelessWidget {
final String text;
const MyText(this.text, {Key key}) : super(key: key); // can const
@override
Widget build(BuildContext context) => Text(text);
}
// usage
const myText("FOO") // compile error
const MyText("FOO")
使用StatelessWidget就比functional widget多了一個可以const的地方。
繼續以上面的myText/MyText為例,當我們在使用它們時:
左邊使用StatlessWidget的widget tree中有出現MyText,右邊使用functional widget的則少了一層myText。畢竟function終究是function,就算使用起來再像也不會真的變成Widget。
這會有什麼問題呢?第一個就是像上面顯示的,影響到Flutter Inspector的呈現。雖然在上面的範例中看起來沒有差很多,但是當我們的widget tree複雜起來時,我們還是會希望能清楚的看到屬於我們自己建立的Widget:
同樣的道理也可以套用在crash時在stack trace上:
除了widget tree的呈現和除錯外,兩者的不同也會導致程式執行時一些更實際的問題,讓我們繼續看下去。
接下來幾個案例比較難用精簡的程式片段表達,因此請大家直接使用來自StackOverflow的DartPad範例。
在這個範例中,如果我們使用StatelessWidget的Title,當counter增加時只有Title會被重建,而如果我們使用functional widget的title(context),會發現counter增加時Home也跟著不斷重建了。這是因為Title在Counter.of(context).value
所使用的context是自己的context,當value有所改變的時候會將自己標示為dirty,
而title所使用的其實是來自Home的context,就變成Home被標示為dirty了。
接下來這個DartPad範例顯示了,functional widget如何造成AnimatedSwitcher無法正確的進行transition。至於原因,AnimatedSwitcher的文件也寫得相當清楚:
簡單來說,AnimatedSwitcher在切換新舊Widget時會檢查Widget.canUpdate
,如果widget key和runtimeType都一樣,就不會進行transition。這正是在functional widget的square/circle發生的狀況,因為對AnimatedWidget來說,它接收到的child都是container
,也都沒有key,就不會進行transition。而StatelessWidget的Square/Circle因為是不同的type,AnimatedSwitcher就可以正確運作。
至於這一個DartPad範例,雖然跟3.的有點像,同樣牽涉到InheritedWidget,但這裡的bug直接導致了crash:
════════ Exception caught by widgets library ═════════════════════════════════════════════
The following NoSuchMethodError was thrown building MyApp(dirty):
The getter 'count' was called on null.
Receiver: null
Tried calling: count
The relevant error-causing widget was:
MyApp file:///D:/Project/_SAMPLE_/flutter_app/lib/main.dart:6:23
When the exception was thrown, this was the stack:
#0 Object.noSuchMethod (dart:core-patch/object_patch.dart:51:5)
#1 Counter.of (package:flutter_app/main.dart:38:66)
#2 home (package:flutter_app/main.dart:65:36)
#3 MyApp.build (package:flutter_app/main.dart:18:15)
#4 StatelessElement.build (package:flutter/src/widgets/framework.dart:4620:28)
...
══════════════════════════════════════════════════════════════════════════════════════════
道理也很簡單,functional widget的home的context其實是來自於MyApp,當我們使用Counter.of(context).value
時,會往上尋找Counter這個類型的InheritedWidget。但實際上Counter卻是在MyApp之下被建立的,因此從home找不到它自然就crash了。
類似的問題也很常出現在Scaffold上,例如:
Widget build(context) {
return MaterialApp(
home: Scaffold(
floatingActionButton: _fab(context),
),
);
}
_fab(BuildContext context) => FloatingActionButton(
onPressed: () => Scaffold.of(context).openDrawer(), // crash!
);
顯然這裡用的context是找不到在它之下的Scaffold的。
以上是幾個你應該使用StatelessWidget而非functional Widget的原因。總結來說,StatelessWidget除了可以利用const和context來達到更好的效能,更少的rebuild,還能避免一些functional widget導致的大大小小的bug。今天我們只講到來自InheritedWidgets和AnimatedSwitcher的問題,但在Flutter提供的無數Widget中,誰知道還有多少會因為我們使用functional widget而造成問題呢?至於functional widget的好處,唯一能想到的可能就只有節省行數吧,但就連這點也可以輕易的靠code generation來避免,所以如果不是非常極端的狀況,還是建議大家稍微花點功夫,寫個StatelessWidget吧!