iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 2
1
Mobile Development

Why Flutter why? 從表層到底層,從如何到為何。系列 第 2

days[1] = "為什麼build()在State裡?"

首先讓我們回顧一下可愛的StatelessWidget

class Foo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

嗯沒什麼特別的,一個Widget subclass,複寫一個build函數來建立子樹,非常簡單直接。

接著再來看看今天的主角StatefulWidget

class Bar extends StatefulWidget {
  @override
  _BarState createState() => _BarState();
}

class _BarState extends State<Bar> {
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

哇事情一下就複雜起來了!Bar有個createState()去建立_BarState(),而_BarState則是繼承了State<Bar>。到這裡勉強還說得通,但是_BarState裡面竟然有個跟StatelessWidget一樣的build函數,這是什麼巫術?為什麼build不是放在StatefulWidget裡面?這樣寫起來既複雜又不對稱,實在是讓人很不舒服。而且State類別聽起來就是單純存放資料的,現在竟然還要負責建立子樹,這不是很奇怪嗎?
這可以說是我最初在學Flutter時產生的第一個疑問,相信很多人第一眼看到StatefulWidget時心理也曾經出現過這樣的想法,今天就讓我們試著來釐清這個問題。

1. 為了保留繼承StatefulWidget時的彈性

解釋這點之前,我們先來瞭解一下AnimatedWidget怎麼運作,這是它的部份原始碼:

abstract class AnimatedWidget extends StatefulWidget {
  /// Override this method to build widgets that depend on the state of the
  /// listenable (e.g., the current value of the animation).
  @protected
  Widget build(BuildContext context);   
}

class _AnimatedState extends State<AnimatedWidget> {
  @override
  Widget build(BuildContext context) => widget.build(context);
}

你可以看到,AnimatedWidget繼承了StatefulWidget,並宣告一個abstract build(注意這不是從StatefulWidget繼承來的,而是AnimatedWidget自己宣告的)。接著_AnimatedStatebuild,透過widget回去呼叫那個abstract build。為什麼要這麼做?讓我們來看看AnimatedWidget的使用方式:

class MyRotatingWidget extends AnimatedWidget {
  const MyRotatingWidget({
    Key key,
    AnimationController controller,
  }) : super(key: key, listenable: controller,);

  Animation<double> get _progress => listenable;

  @override
  Widget build(BuildContext context) {
    return Transform.rotate(
      angle: _progress.value,
      child: FlutterLogo(),
    );
  }
}

動畫一定是有狀態_progress的,而狀態就須要StatefulWidget來管理,但如果我們唯一的狀態只有_progress,其它部份都是...stateless呢?我們還需要自己分別繼承StatefulWidgetState嗎?這就是AnimatedWidget幫你處理的事情,它幫你繼承去StatefulWidgetState並管理_progress。然後它宣告一個build函數給你覆寫,就好像你是在繼承StatelessWidget一樣。你不須要自己建立和管理狀態,也不須要知道AnimatedWidget背後其實有著StatefulWidget這樣的實作細節

好,最後讓我們回到一開始的問題,如果我們把build搬回StatefulWidget會怎樣呢?

abstract class StatefulWidget extends Widget {
  Widget build(BuildContext context, State state);
}
// option 1: extra build function 
// result  : confusing multiple build function
class AnimatedWidget extends StatefulWidget {
  @protected
  Widget build(BuildContext context);
}
// option 2: expose build function from StatefulWidget as-is 
// result  : unnecessarily expose [state], which should be an implementation detail
class AnimatedWidget extends StatefulWidget {
    // do nothing
}

這時候StatefulWidgetbuild就必須接收一個State物件,畢竟你終究須要存放在裡面的狀態變數來建立子樹。而AnimatedWidget繼承StatefulWidget時就同時繼承了build(context, state),也就進一步把state這個實作細節暴露出去了。

AnimatedWidget只是其中一個例子,基本上任何時候,當我們想建立一個custom abstract widget來讓其它class繼承,幫child class管理狀態並隱藏起來時,如果有build(context, state)存在在StatefulWidget裡面,就會導致實作細節的暴露了。

2. Closure中隱含的this造成的bug

class MyButton extends StatefulWidget {
  MyButton(this.color);
  
  final Color color;
  
  @override
  Widget build(BuildContext context, State state) => FlatButton(
      onPressed: () { print('color: $color'); },
    );
}

假設今天有個MyButton繼承了我們新的StatefulWidget,其中有個屬性colorMyButton第一次被parent建立時被傳入Colors.blue,這時print裡的$color會是blue沒錯。但如果parent決定重新建立MyButton並傳入Colors.green,這時候print$color卻依然會是blue。原因在於雖然MyButton是新的實例,但是FlatButton卻不會被重建,因為它沒有任何改變。也就是說這時onPressed被賦予的Closure () { print('color: $color'); } 也一樣是舊的Closure,而這裡面隱含取用的this也是舊的this,也就是舊的MyButton

class MyButtonState extends State<MyButton> {

  @override
  Widget build(BuildContext context) => FlatButton(
      onPressed: () { print('color: $widget.color'); },
    );
}

相較之下,如果我們的build是在State裡面,因為我們是透過widget.color來取得color,而這裡的widget參照是會在StatefulWidget重新建立時被更新的,也就避免了上述的bug。


以上兩點是官方針對這個問題給出的回應,聽起來合理但好像又不是那麼有說服力。畢竟Stateless/StatefulWidget是Flutter最基本也最常被使用的元件,就為了這兩個感覺不怎麼嚴重的問題而讓整個設計複雜化,感覺是不是有點太小題大作了?說到底這一切問題的根源還是在於,一個被命名為State的類別負擔了太多不該是State的責任。如果一開始把它叫做ViewModelController啥的,或許大家就不會那麼混亂了?


上一篇
days[0] = "為什麼你應該現在開始學習Flutter?"
下一篇
days[2] = "為什麼選擇Dart?"
系列文
Why Flutter why? 從表層到底層,從如何到為何。30

尚未有邦友留言

立即登入留言