iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 18
0
Mobile Development

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

days[17] = "為什麼你應該嘗試從Provider升級到Riverpod?(下)"

讓我們從複習一下InheritedWidget開始,這是我能做到的最簡單的InheritedWidget範例。可以看到,除了我們必須改為繼承長得跟StatelessWidget/StatefulWidget很不一樣的InheritedWidget,還有必須呼叫名字長得可笑的dependOnInheritedWidgetOfExactType之外,其實看起來還不算太糟對嗎?

不過,通常我們想傳遞的是會變動的State,但InheritedWidget卻不是StatefulWidget,所以事情就開始有趣起來了。我們必須用一個StatefulWidget來管理狀態,進行更新,把狀態傳遞給InheritedWidget,再由InheritedWidget提供給子樹上的Widget。

我可以理解Flutter團隊應該是為了單一責任原則(Single Responsibility Principle)而選擇這樣設計,讓InheritedWidget唯一的責任,就是把變數提供給子樹上的Widget。不過我認為Flutter團隊沒有想清楚的是,我們會想傳遞下去的變數,絕大多數就是我們提昇上來的狀態,也就須要變動和更新。

因為我們的主角還是Riverpod,我就不一一示範InheritedWidget的其它問題了,簡單來說:

  1. 寫起來麻煩,讀起來複雜
  2. 狀態多時造成更深的嵌套
  3. 因為根據型別向上尋找InheritedWidget,無法同時提供相同型別的變數
  4. 找不到時可能產生執行時期例外
  5. 依賴Widget,無法解決Widget之外的問題

關於5.可能須要額外說明一下。除了Widget會不斷複合之外,我們的整個程式還有很多可能發生複合的地方,例如BloC/ViewModel, Interactor/UseCase, Service...如果今天我們想在這些地方穿越多層暴露狀態,顯然就不能使用InheritedWidget。但是如果我們找到其它解決方案,可以在任何型別的樹中自由地暴露狀態,那我們還須要InheritedWidget嗎?

好,回來看看Provider能解決多少問題吧,把上一個範例改成使用Provider看看。首先程式碼絕對大有改善,不只是行數減少而已,邏輯也清楚很多。那麼嵌套的問題呢?當然就是靠MultiProvider。不僅如此,Provider還免費提供了lazy-loading,當你想透過MultiProvider再最上層提供各種昂貴的service時,肯定會須要它。最後就是最近統一起來的context.watch, context.read, context.select,透過簡單且一致的API,提供了InheritedWidget難以實現的各種變數取用功能。

當然Provider也不是完美的。雖然解決了問題1.和2.,再加上一些額外功能,已經是非常大的進步,也因此它才會在社群裡如此火紅。但畢竟它背後還是依賴InheritedWidget,問題3.、4.、5.可以說是完全沒變。再加上隨著Provider的火紅,人們對於它的要求也越來越多了,於是現在我們有這些問題:

  1. 因為根據型別向上尋找Provider,無法同時提供相同型別的變數
  2. 找不到時可能產生執行時期例外
  3. 依賴Widget,無法解決Widget之外的問題
  4. 無法簡單地組合多個Provider(ProxyProvider使用上相對複雜一點)
  5. 無法再沒人使用state的時候dispose state(只能再dispose widget時dispose state)
  6. 無法建立private Provider

顯然3,4,5是不可能解決的,只要Provider還是個widget,還是依賴InheritedWidget的話。因此,Provider的作者Rémi Rousselet決定,全部重零開始吧!打造一個不依賴InheritedWidget,甚至不依賴Flutter的解決方案。於是,終於,萬眾矚目的Riverpod出現了!

馬上來看看它表現如何吧。首先整個程式邏輯又更簡潔了一點,再來因為Riverpod裡的Provider已經不再是Widget了,不需要塞進widget tree,自然就沒有嵌套的問題。也就是說一開始Provider所解決的,InheritedWidget的問題1和2,再這裡也更進步了。那麼Provider繼承自InheritedWidget的問題3,4,5和新增的6,7,8呢?因為我們現在是直接透過變數來存取Provider,而不是透過型別,問題3和4就自動消失了,同時8也可以簡單實現。接下來我們繼續看看剩下的5,6,7:

至於5,我們就來看看怎麼在非Flutter環境使用Riverpod吧。這裡的ProviderContainer作用就跟上個範例裡的ProviderScope一樣,用來存放我們所有的state。我們可以看到它其實有點像Observer模式了,我相信它在純Dart環境能做得到的所有事,用stream/rxDart都能做到,但至少它跟Provider(package)比起來,就多了很多可以發揮的地方。這裡作者將Riverpod拆成了riverpod, flutter_riverpod, hooks_riverpod三個套件,如果我們想在一個純Dart模組使用(ex. Domain),只需要加入riverpod就好了。

接著來看看問題6怎麼解決:

final cityProvider = Provider((ref) => 'London');
final weatherProvider = FutureProvider((ref) async {
  final city = ref.watch(cityProvider);
  return fetchWeather(city: city);
});

就這樣,我都不用開DartPad了,也不須要使用什麼特別的ProxyProvider23456,簡單明瞭。

至於問題7,那就是靠autoDispose了:

final userProvider = StreamProvider.autoDispose<User>((ref) { .... })

任何種類的Provider都搭配autoDispose使用,只要沒有人在watch就會自動被dispose掉。

到這裡我們列出的所有問題都解決了!當然除了這些之外,Riverpod還有提供了許多Provider沒有的功能,但這篇文章畢竟不是Riverpod教學文,就不一一介紹了。我們主要目的還是在於,確認Riverpod真的有解決了從InheritedWidget到Provider所無法解決的問題。如果這些的確是你曾經遇過,或你能夠預想未來可能會遇到的問題,Riverpod就很值得你嘗試看看。


上一篇
days[16] = "為什麼你應該嘗試從Provider升級到Riverpod?(上)"
下一篇
days[18] = "Isolate是怎麼運作的?"
系列文
Why Flutter why? 從表層到底層,從如何到為何。30

尚未有邦友留言

立即登入留言