圍觀Github上Flutter評論最多的Issue

那個(gè)評論最多的Issue

關(guān)注Flutter的同學(xué)們可能經(jīng)常會去Github上看看Flutter現(xiàn)狀?,F(xiàn)在star數(shù)量已經(jīng)是10.4w了,但是,近一年以來處于open狀態(tài)的issue數(shù)量一直徘徊在7k+。這一方面說明Flutter確實(shí)火爆,另一方面open issue這平穩(wěn)的走勢也確實(shí)讓廣大開發(fā)者對Flutter的未來有些許擔(dān)心。這個(gè)問題可能大家各自會有不同的看法,這里我就不展開說了。

這7k多open(以及37k+closed)的issue中,評論最多就是這條:Reusing state logic is either too verbose or too difficult,在我寫這篇文章的時(shí)候已經(jīng)有407條評論。足足是評論數(shù)第二多issue的兩倍還有余。issue的提出者是@rrousselGit,他是Flutter官方推薦的狀態(tài)管理庫Provider的作者,也是flutter_hook的作者。

評論最多的Issue

到底是什么樣的issue這么的火爆呢?把上面的issue標(biāo)題翻譯過來就是復(fù)用狀態(tài)邏輯要么太麻煩要么太困難。狀態(tài)邏輯是什么,太麻煩和太困難又是指什么呢?由于篇幅有限,這里就不引用issue的全部內(nèi)容。感興趣的同學(xué)可以點(diǎn)上面的鏈接看全文。但我的感覺是這個(gè)issue想表達(dá)的東西和我們這些Flutter開發(fā)者息息相關(guān),以后有可能會完全改變當(dāng)前的開發(fā)方式。所以希望大家能早點(diǎn)關(guān)注,以便為未來的變化做好準(zhǔn)備。以下就狀態(tài)邏輯復(fù)用方式的問題做一個(gè)介紹。

狀態(tài)邏輯復(fù)用問題

我們都知道Flutter體系里有兩種Widget,無狀態(tài)的StatelessWidget和有狀態(tài)的StatefulWidgetWidget是不可變的。如果需要在Element生命周期內(nèi)擁有可變的狀態(tài),那就只好把這些可變的東西都塞進(jìn)State里面了。可變的狀態(tài)其實(shí)就是個(gè)時(shí)間的函數(shù),S = f(t)。如果說S是狀態(tài)值,那么這個(gè)函數(shù)f()就是狀態(tài)邏輯了,而時(shí)間t的取值范圍是Element的生命周期??勺儬顟B(tài)值是狀態(tài)邏輯的時(shí)間函數(shù)值。這里的狀態(tài)邏輯在我們實(shí)際開發(fā)中遇到的可能是從網(wǎng)絡(luò)獲取數(shù)據(jù),加載圖片,播放動畫等等。所以這里討論的復(fù)用狀態(tài)邏輯就是在討論這個(gè)f()如何在不同的Widget之間復(fù)用。

那我們先來看看原生Flutter中如何來做復(fù)用。這里假設(shè)我們有一個(gè)自己實(shí)現(xiàn)的特殊的網(wǎng)絡(luò)請求類MyRequest,在我們的app中只要是網(wǎng)絡(luò)請求都需要使用這個(gè)類。那么一般的實(shí)現(xiàn)是這個(gè)樣子的:

class Example extends StatefulWidget {
  @override
  _ExampleState createState() => _ExampleState();
}

class _ExampleState extends State<Example> {

  MyRequest _myRequest = MyRequest();

  @override
  void initState() {
    super.initState();
    _myRequest.start();
  }

  @override
  void dispose() {
    _myRequest.cancel();
    super.dispose();
  }
}

我們需要自定義一個(gè)StatefulWidget類和一個(gè)對應(yīng)的State類。在State內(nèi)部實(shí)例化MyRequest, 在initStatedispose內(nèi)分別做初始化和清理釋放。

要復(fù)用的話就需要把上面做的事情在其他Widget那里重復(fù)。情況可能會再稍微復(fù)雜一些,上面的例子Example這個(gè)Widget內(nèi)部沒有任何屬性,它的State沒有對外依賴。所以上面的實(shí)現(xiàn)沒什么問題。但當(dāng)我們的請求需要外部傳入一個(gè)用戶名uerId的時(shí)候??赡芫妥兂上旅孢@樣的了:

class Example extends StatefulWidget {
  //多了個(gè)userId.
  final userId;
  
  const Example({Key key, @required this.userId})
      : assert(userId != null),
        super(key: key);

  @override
  _ExampleState createState() => _ExampleState();
}

class _ExampleState extends State<Example> {

  MyRequest _myRequest = MyRequest();

  @override
  void initState() {
    super.initState();
    _myRequest.start(widget.userId);
  }
  // 需要重寫didUpdateWidget。當(dāng)userId變化的時(shí)候重新做請求
  @override
  void didUpdateWidget(Example oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.userId != oldWidget.userId) {
      _myRequest.cancel();
      _myRequest.start(widget.userId);
    }
  }

  @override
  void dispose() {
    _myRequest.cancel();
    super.dispose();
  }
}

多了個(gè)userId之后,我們就需要重寫didUpdateWidget了。狀態(tài)邏輯的復(fù)用就更加復(fù)雜和繁瑣了。

更進(jìn)一步,如果State中有多個(gè)請求,那復(fù)雜度就更上一個(gè)臺階了。如果要添加/刪除一個(gè)MyRequest就需要至少在initState,didUpdateWidgetdispose等函數(shù)中做操作。因?yàn)橐粋€(gè)StatefulWidget對應(yīng)一個(gè)State,所以復(fù)用其實(shí)就是在做零碎的復(fù)制粘貼。這顯然是繁瑣且容易出bug的操作。

解決方案是怎樣的

通過上面的分析。我們就可以得出解決方案要滿足哪些標(biāo)準(zhǔn)了,新方案以下就稱為“模塊”吧。
首先,就是“模塊”應(yīng)該是包含有一塊獨(dú)立的狀態(tài)邏輯。比如上面說的一個(gè)網(wǎng)絡(luò)請求,一次IO操作等等?!澳K”應(yīng)該是與UI無關(guān)的,所以“模塊”內(nèi)部最好不依賴于外部的Widget。

其次,就是我們也看到了,原生方式繁瑣復(fù)雜的一個(gè)原因是一個(gè)獨(dú)立的狀態(tài)邏輯被切分開來分散到了State的生命周期函數(shù)中了。所以新的方案最好能讓程序自己去處理“模塊”的生命周期回調(diào)而不需要用戶手動操作。

再次,“模塊”可以組合起來提供更復(fù)雜的狀態(tài)邏輯。也就是說如果狀態(tài)邏輯可以被表達(dá)為S = f(t),那么組合起來看起來會是這樣的S = f(a(t),t)或者S = f(a(t),b(t),t)。Widget其實(shí)就是這樣組合起來的。

最后,就是新方案在性能上不能有不可接受的下降。不管是在時(shí)間(響應(yīng))還是空間(內(nèi)存)方面都要對比原生做法不能有較大的降低。

總結(jié)下來就是以下幾點(diǎn):

  • 獨(dú)立性,“模塊”包含一個(gè)獨(dú)立的狀態(tài)邏輯。
  • 自管理,自動處理initState等生命周期。
  • 可組合,“模塊”可以組合起來提供更復(fù)雜的狀態(tài)邏輯
  • 性能優(yōu),性能上不應(yīng)該有不可接受的劣化。

可能的解決方案

明確了目標(biāo)以后,接下來看看issue中討論的解決方案有哪些,有什么樣的優(yōu)缺點(diǎn)。

Mixin

使用Mixin改造以后的狀態(tài)邏輯可能是像這樣的:

mixin MyRequestMixin<T extends StatefulWidget> on State<T> {
   MyRequest _myRequest = MyRequest();
   
   MyRequest get myRequest => _myRequest;

  @override
  void initState() {
    super.initState();
    _myRequest.start();
  }

  @override
  void dispose() {
    _myRequest.cancel();
    super.dispose();
  }
}

使用起來是這樣的:

class Example extends StatefulWidget {
  @override
  _ExampleState createState() => _ExampleState();
}

class _ExampleState extends State<Example> with MyRequestMixin<Example> {
  @override
  Widget build(BuildContext context) {
    final data = myRequest.data;
    return Text('$data');
  }
}

關(guān)于具體如何使用Mixin來分離視圖與數(shù)據(jù),可以參考我的另外一篇文章

  • Mixin方式滿足上述條件中的獨(dú)立性和性能優(yōu)兩個(gè)指標(biāo),但是自管理只是部分滿足。當(dāng)Widget里不含有Mixin需要的參數(shù)的時(shí)候是沒有問題的。可當(dāng)Widget里含有Mixin需要的參數(shù)的時(shí)候,例如上面說的userId。那么代碼就飄紅了:
    image
  • 組合能力方面也有缺陷。一個(gè)State只能混入一個(gè)同類型的Mixin。所以給一個(gè)State混入多個(gè)同類型的狀態(tài)邏輯是不可行的。
  • 還有一個(gè)缺陷是當(dāng)不同的Mixin定義了相同的屬性時(shí)會造成沖突。

Builder

Buidler模式其實(shí)在Flutter框架里面已經(jīng)有很多現(xiàn)成的例子,比如StreamBuilder,FutureBuilder等等。
使用Builder改造以后的MyRequest狀態(tài)邏輯可能是像這樣的:

class MyRequestBuilder extends StatefulWidget {

  final userId;

  final Widget Function(BuildContext, MyRequest) builder;

  const MyRequestBuilder({Key key, @required this.userId, this.builder})
      : assert(userId != null),
        assert(builder != null),
        super(key: key);

  @override
  _MyRequestBuilderState createState() => _MyRequestBuilderState();
}

class _MyRequestBuilderState extends State<MyRequestBuilder> {

  MyRequest _myRequest = MyRequest();

  @override
  void initState() {
    super.initState();
    _myRequest.start(widget.userId);
  }

  @override
  void didUpdateWidget(MyRequestBuilder oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.userId != oldWidget.userId) {
      _myRequest.cancel();
      _myRequest.start(widget.userId);
    }
  }

  @override
  void dispose() {
    _myRequest.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return widget.builder(context, _myRequest);
  }
}

用起來是這樣的:

class Example extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MyRequestBuilder(
      userId: "Tom",
      builder: (context, request) {
        return Container();
      },
    );
  }
}

可見,Builder模式基本上是滿足上面那幾個(gè)條件的。是一種目前看來可行的狀態(tài)邏輯復(fù)用方式。但也有另外幾個(gè)缺陷:

  • 多個(gè)Builder組合起來的時(shí)候代碼可讀性下降:
class Example extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MyRequestBuilder(
      userId: "Tom",
      builder: (context, request1) {
        return MyRequestBuilder(
          userId: "Jerry",
          builder: (context, request2) {
            return Container();
          },
        );
      },
    );
  }
}

Builder模式其實(shí)并不是一種優(yōu)雅的解決辦法。本來是并列關(guān)系的狀態(tài)邏輯被組合成了父子關(guān)系。我們想要的應(yīng)該是這樣的而不是嵌套起來的模式:

    MyRequest request1 = MyRequest();
    MyRequest request2 = MyRequest();
    ... //使用request1和request2

這就是Builder模式的一個(gè)缺陷,如果嵌套的Builder比較多的話縮進(jìn)會非常難看。

  • Element樹里增加了節(jié)點(diǎn)??赡軐π阅苡幸恍┯绊憽?/li>
  • 最后就是狀態(tài)邏輯無法在Builder之外不可見。外層build函數(shù)無法直接訪問request1,一種變通辦法就是使用GlobalKey,但這樣的話復(fù)雜性又增加了。

Properties/MicroState

這個(gè)解決方案是把狀態(tài)邏輯封裝到一個(gè)個(gè)類似于State的類里面,稱之為Property,使用的時(shí)候會把封裝好的Property集中安裝到宿主State中,然后由宿主State來自動處理Property的生命周期回調(diào)。
封裝長這樣:

// Property 接口,和State生命周期回調(diào)一致
abstract class Property {

  void initState();

  void dispose();
}
// Property實(shí)現(xiàn)
class MyRequestProperty extends Property {

  MyRequest _myRequest = MyRequest();

  @override
  void initState() {
    _myRequest.start();
  }

  @override
  void dispose() {
    _myRequest.cancel();
  }
}
// 宿主 State
abstract class PropertyManager extends State {
  // 復(fù)用的狀態(tài)邏輯保存在這里
  final properties = <Property>[];

  @override
  void initState() {
    super.initState();
    // 遍歷回調(diào)initState
    for (final property in properties) {
        property.initState();
     
    }
  }

  @override
  void dispose() {
    super.dispose();
     // 遍歷回調(diào)dispose
    for (final property in properties) {
      property.dispose();
    }
  }
}

這種模式的關(guān)鍵點(diǎn)其實(shí)就是在宿主State中用一個(gè)列表來保存添加進(jìn)來的Property。然后宿主在自己的生命周期回調(diào)里遍歷Property,然后調(diào)用它們相應(yīng)的回調(diào)函數(shù)??梢娺@種方式滿足獨(dú)立性和性能方面的要求,自管理在不依賴Widget屬性的情況下也還行,但是當(dāng)有像userId這樣的依賴的時(shí)候則需要宿主重寫didUpdateWidget,提取出依賴的屬性然后再發(fā)送給對應(yīng)的Property??梢?code>Property方式也有和Mixin類似的缺陷。另外在組合方面,當(dāng)Property是并列關(guān)系的時(shí)候也沒什么問題,但是如果要把幾個(gè)Property組合成大一點(diǎn)的Property就比較麻煩一些了。

Hooks

最后就是這個(gè)評論數(shù)最高issue的主角了,Hooks。如果引入Hooks的話,MyRequest的狀態(tài)邏輯復(fù)用就會變成下面這個(gè)樣子了:

// 不再需要StatefulWidget
class MyRequestWidget extends HookWidget {

  final userId;
  const MyRequestBuilder({Key key, @required this.userId)
      : assert(userId != null),
        super(key: key);
        
  @override
  Widget build(BuildContext context) {
    // 一個(gè)函數(shù)搞定一切
    final myRequest = useMyRequest(userId: userId);
    return Container();
  }
}

瞬間變得無比簡潔有木有?沒有initState,didUpdateWidgetdispose等生命周期回調(diào),沒有Builder那樣的嵌套,沒有零碎的復(fù)制粘貼,甚至連StatefulWidget也都不再需要了。只需要在build中加一行useXXX函數(shù)就可以了。獨(dú)立性,自管理,性能都不存在問題,組合性上也不存在問題。具體可以參考我之前介紹Hooks的文章《Flutter Hooks 使用及原理》

缺點(diǎn)嘛就是Hooks太過激進(jìn)(簡潔),有些方面和Flutter的理念是相抵觸的。從State的設(shè)計(jì)就能看出來,每個(gè)生命周期回調(diào)都給你整的明明白白,什么階段做什么事情,都讓開發(fā)者能自己掌控。而現(xiàn)在呢?沒有了生命周期,沒有了State,所有這一切全部被一個(gè)build函數(shù)里的useXXX所替代。這可能會讓習(xí)慣掌控生命周期的開發(fā)者感到惶恐,這個(gè)函數(shù)的背后到底發(fā)生了什么?會不會有什么不可預(yù)知的后果?我們一直都謹(jǐn)記在build函數(shù)中不可以調(diào)用復(fù)雜耗時(shí)函數(shù),build函數(shù)應(yīng)該保持純凈,只能做和構(gòu)建相關(guān)的事情,其他的初始化,清理等等工作應(yīng)該在相應(yīng)的回調(diào)里去做才對啊。可是這里的useXXX似乎把這些活全都安排了,這不合適吧。

這也就是這個(gè)issue能一口氣蓋了4百多層樓的原因,其實(shí)背后就是這兩種理念(甚至是OOP與FP之間)的交鋒。

通過圍觀我們能學(xué)到什么

通常我們學(xué)習(xí)新技術(shù)的時(shí)候都是去看別人寫好的文檔,去研讀別人寫好的源代碼。照貓畫虎的寫一寫自己的代碼,這樣下來只能說是會用了而已。文檔和源代碼都已經(jīng)是成品,你看到的成品是一個(gè)樣子,但背后可能會有很多的草稿,為什么這個(gè)設(shè)計(jì)能脫穎而出,必定是通過不停的交流和迭代才擊敗了其他競爭者。我們看到的掌握了API只能說是知其然可能卻不知其所以然。要知其所以然,就要參與到設(shè)計(jì)過程中去,即使還不能提出自己的觀點(diǎn),但持續(xù)關(guān)注各方大牛的討論也絕對受益匪淺。

  • 通過對一個(gè)問題的剖析能了解到更多的信息,之前可能是知其一而不知其二,但是通過圍觀可能會獲得我們不知道的其二。

  • 通過對一個(gè)解決方案的正反兩方面的交鋒,能更清楚的知道其來龍去脈,優(yōu)缺點(diǎn)。

  • 通過對正方兩方互相交換意見(吵架)的圍觀可以學(xué)習(xí)到怎樣的交流方式是建設(shè)性的,錯(cuò)誤的交流方式會使交流越來越偏離交流的目的。不僅浪費(fèi)時(shí)間,而且對團(tuán)隊(duì),組織,或社區(qū)有破壞性作用,是要避免的。

  • 通過圍觀也可以學(xué)到如何來掌控交流的方向,敏銳察覺交流進(jìn)程中的異常狀況,如何及時(shí)采取措施確保交流回到正確的軌道上來。

所以我建議大家,有事沒事多多關(guān)注業(yè)界新動向,不僅僅是這個(gè)具體的issue,確實(shí)能學(xué)到看文檔得不到的知識。

最后,回到本文這個(gè)issue。對于hooks背后兩種理念的爭議我也沒有結(jié)論,但是我建議大家可以自己來評估一下,也許在自己的項(xiàng)目里用hooks做那么一兩個(gè)頁面嘗試一下,梨子是啥滋味總歸要自己嘗一下。不過據(jù)有些人說,hooks屬于那種用了就回不去類型的:)另外,除了React, Vue、iOS SwiftUI以及Android Jetpack Compose也都引入了類似hooks的實(shí)現(xiàn)。

(全文完)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容