Flutter Riverpod 3.0 發布,大規模重構下的全新狀態管理框架

image

在之前的 《註解模式下的 Riverpod 有什麼特別之處》 我們聊過 Riverpod 2.x 的設計和使用原理,同時當時我們就聊到作者已經在開始探索 3.0 的重構方式,而現在隨著 Riverpod 3.0 的發布,riverpod 帶來了許多細節性的變化。

當然,這也帶來了使用方式上的變動。

廢話不多說,首先 Riverpod 3.0 與 2.0 的對比,新增的功能有:

  • 自動重試失敗的 Provider: 這是 3.0 的一個核心特性,當一個 Provider 出現計算失敗時(如網路錯誤導致),Riverpod 不會立刻報錯,而是自動嘗試重新計算,從而讓對短暫性錯誤有更強的恢復能力
  • 暫停/恢復支持: 當一個 Widget 不在螢幕上時,與之關聯的 Provider 監聽器現在會自動暫停
  • 離線和變更 (Mutation) 支持 (實驗性): Riverpod 3.0 引入了對離線資料快取和 “mutation” 操作的實驗性支持,讓處理資料持久化和異步操作(如表單提交)變得更加容易
  • 簡化的 API: 通過合併 AutoDisposeNotifierNotifier 等介面,API 變得更加統一和簡潔

同時 3.0 也引入了一些破壞性改動:

  • 傳統 Provider 的遷移: StateProvider, StateNotifierProviderChangeNotifierProvider 這些在 3.0 屬於“傳統”API,它們雖然沒有被移除,但都被移至一個新的 legacy 導入路徑下,推薦開發者使用新的 Notifier API
  • 統一使用 == 進行更新過濾: 在 3.0 版本所有的 Provider 都使用 == (相等性) 而非 identical 來判斷狀態是否發生變化,從而決定是否需要重建
  • 簡化的 Ref 和移除的子類: Ref 不再有泛型參數,並且像 ProviderRef.stateRef.listenSelf 這樣的屬性和方法都被移至 Notifier,同時所有 Ref 的子類(如 FutureProviderRef)都已被移除,現在可以直接使用 Ref
  • 移除 AutoDispose 介面: 自動釋放功能被簡化,不再需要獨立的 AutoDisposeProvider, AutoDisposeNotifier 等介面,現在所有 Provider 都可以是 auto-dispose
  • ProviderObserver 介面變更: ProviderObserver 的方法簽名發生了變化,現在傳遞的是一個 ProviderObserverContext 對象,其中包含了 ProviderContainerProviderBase 等信息

接下來我們詳細講解這些變化。

自動重試失敗的 Provider

在 Riverpod 3.0 中,Provider 現在預設會自動重試失敗的計算,這意味著如果一個 Provider 因為網路波動、服務暫時不可用等瞬時錯誤而構建失敗,它不会立即报错,而是會自動嘗試重新計算,直到成功為止

這個功能是預設開啟的,我相信你第一想法就是我不需要,在某些情況下你可能希望禁用或自定義重試邏輯:

  • 全局禁用/自定義: 你可以在 ProviderScopeProviderContainer 的頂層進行全局配置,透過設置 retry 參數,可以精細地控制重試邏輯,例如根據錯誤類型或重試次數來決定是否繼續重試,以及重試的間隔時間:
void main() {
  runApp(
    ProviderScope(
      // 全局禁用自動重試
      retry: (retryCount, error) => null,
      child: MyApp(),
    ),
  );
}
  • 針對特定 Provider 禁用/自定義: 可以在定義單個 Provider 時,透過其 retry 參數進行獨立的配置:
@Riverpod(retry: retry)
class TodoList extends _TodoList {
  // 從不重試這個特定的 provider
  static Duration? retry(int retryCount, Object error) => null;

  @override
  List<Todo> build() => [];
}

暫停/恢復支持

為了優化資源使用,Riverpod 3.0 引入了暫停/恢復機制。當一個 Widget(及其關聯的 Provider 監聽器)不在螢幕上時,監聽器會自動暫停,這個行為是預設啟用的,並且看起來不支持全局關閉,你可以透過 Flutter 的 TickerMode 來手動控制監聽器的暫停行為:

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return TickerMode(
      enabled: false, // 這會暫停監聽器
      child: Consumer(
        builder: (context, ref, child) {
          // 這個 "watch" 將會暫停
          // 直到 TickerMode 設置為 true
          final value = ref.watch(myProvider);
          return Text(value.toString());
        },
      ),
    );
  }
}

image

離線和變更 (Mutation) 支持 (實驗性)

Riverpod 3.0 引入了兩個實驗性功能:

  • 離線支持: 允許你輕鬆地將 Provider 的狀態持久化,以便在應用重啟或離線時恢復
  • 變更 (Mutation) 支持: 提供了一種結構化的方式來處理異步操作,例如用戶登錄、提交表單或任何會改變應用狀態的動作
// A mutation to track the "add todo" operation.
// The generic type is optional and can be specified to enable the UI to interact
// with the result of the mutation.
final addTodo = Mutation<Todo>();

// We listen to the current state of the "addTodo" mutation.
// Listening to this will not perform any side effects by itself.
final addTodoState = ref.watch(addTodo);

switch (addTodoState) {
  case MutationIdle():
    // Show a button to add a todo
  case MutationPending():
    // Show a loading indicator
  case MutationError():
    // Show an error message
  case MutationSuccess():
    // Show the created todo
}

API 變動

Riverpod 3.0 對其核心 API 進行了大幅簡化和統一,具體有:

1、合併 AutoDispose 介面

在之前的版本中,有大量帶有 AutoDispose 前綴的介面,如 AutoDisposeProviderAutoDisposeNotifier,而在 3.0 中這些介面被統一了,現在,你只需要使用 ProviderNotifier 等核心介面:

//**V2.0:**
// 使用 .autoDispose 修飾符
final myProvider = Provider.autoDispose((ref) {
  return MyObject();
});

//**V3.0:**
// 1. 對於手寫 Provider
final myProvider = Provider(
  (ref) => MyObject(),
  isAutoDispose: true, // 使用 isAutoDispose 參數
);

// 2. 對於程式碼生成的 Provider
@Riverpod(keepAlive: false) // keepAlive: false 是預設行為,等同於 autoDispose
int myProvider(MyProviderRef ref) {
  return 0;
}

2、移除 FamilyNotifier 變體

類似於 AutoDispose 的簡化,FamilyNotifierFamilyAsyncNotifier 等家族變體也被移除了,現在你只需要使用 NotifierAsyncNotifier 等核心 Notifier,並透過構造函數來傳遞參數

final provider = NotifierProvider.family<CounterNotifier, int, String>(CounterNotifier.new);

-class CounterNotifier extends FamilyNotifier<int, String> {
+class CounterNotifier extends Notifier<int> {
+  CounterNotifier(this.arg);
+  final String arg;

  @override
-  int build(String arg) {
+  int build() {
     // 在這裡使用 `arg`
      return 0;
  }
}

3、 Provider 變動

統一在 Riverpod 3.0 中,StateProvider, StateNotifierProvider, 和 ChangeNotifierProvider 被歸類為“傳統(legacy)”API,這新的 Notifier API 更加靈活、功能更強大,並且與程式碼生成(code generation)的結合更緊密,可以顯著減少樣板程式碼,現在推薦使用:

  • Notifier: 用於替代 StateNotifierProvider,管理同步狀態,它是個可以被監聽的類,並且可以定義自己的公共方法來修改狀態。
  • AsyncNotifier: 用於替換處理異步操作的 StateNotifierProviderFutureProvider,它專門用於管理異步狀態(如從網路獲取資料),並內置了對加載、資料和錯誤狀態的處理
  • StreamNotifier: 用於替代 StreamProvider

V2.0 :

import 'package:flutter_riverpod/legacy.dart'; // 需要使用 legacy 導入

// Before:
final valueProvider = FutureProvider<int>((ref) async {
  ref.listen(anotherProvider, (previous, next) {
    ref.state++;
  });

  ref.listenSelf((previous, next) {
    print('Log: $previous -> $next');
  });

  ref.future.then((value) {
    print('Future: $value');
  });

  return 0;
});

V3.0 (新的 Notifier API):

// After
class Value extends AsyncNotifier<int> {
  @override
  Future<int> build() async {
    ref.listen(anotherProvider, (previous, next) {
      ref.state++;
    });

    listenSelf((previous, next) {
      print('Log: $previous -> $next');
    });

    future.then((value) {
      print('Future: $value');
    });

    return 0;
  }
}

final valueProvider = AsyncNotifierProvider<Value, int>(Value.new);

可以看到,如果用的是 2.x 的註解,其實並不需要變動什麼。

所以,現在推薦的 API 是 NotifierAsyncNotifier,它們是基於類的 Provider,其原理是將狀態的定義 (build 方法)修改狀態的方法封裝在同一個類中,目的在於:

  • 邏輯內聚: 與特定狀態相關的所有程式碼都在一個地方,易於管理
  • 程式碼更簡潔: 結合程式碼生成,你只需要定義一個類,Provider 會被自動創建
  • 類型安全: 你可以定義強類型的公共方法來修改狀態,而不是直接暴露狀態物件本身

4、統一使用 == 進行更新過濾

這個改動統一了 Provider 的行為:

  • identical: 之前它檢查兩個引用是否指向同一個記憶體地址,兩個內容完全相同的不同物件,identical 會返回 false
  • == (相等性): 現在檢查兩個物件是否相等,對於自定義類,你可以重寫 == 操作符來定義相等的標準(例如,如果兩個 User 物件的 id 相同,則認為它們相等)

具體是,在 V2.0 中某些 Provider(如 Provider)使用 identical 來判斷狀態是否變化,而另一些則使用 ==,這意味著,即使你提供了一個內容相同但實例不同的新物件,前者也不會通知監聽者更新,因為它認為物件“沒有變化”。

在 V3.0 中,所有 Provider 都預設使用 == 來比較新舊狀態,如果新舊狀態透過 == 比較後結果為 true,則不會通知監聽者進行重建

舉個例子,假設你有一個 User 類,並且你已經重寫了 == 操作符:

class User {
  final String name;
  User(this.name);

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is User && runtimeType == other.runtimeType && name == other.name;

  @override
  int get hashCode => name.hashCode;
}

現在,有一個 Provider 返回 User 物件:

final userProvider = Provider((ref) => User('John'));

在某個操作後,你讓這個 Provider 返回了一個新的 User 實例,但 name 屬性仍然是 'John':

  • V2.0 (使用 identical): 由於新舊 User 物件是不同的實例(記憶體地址不同),identical 會返回 false,UI 會重建。
  • V3.0 (使用 ==): 由於我們重寫了 ==,只要 name 相同,user1 == user2 就會返回 true。因此,Riverpod 會認為狀態沒有變化,UI 不會重建,從而避免了不必要的刷新。

另外,如果你需要自定義這種行為,可以在你的 Notifier 中重寫 updateShouldNotify 方法。

5、簡化的 Ref 和移除的子類

這個改動的核心目的是簡化 API 和提升類型安全,保證 API 更統一,因為以前根據 Provider 類型的不同(如 Provider vs FutureProvider),ref 的類型也不同(ProviderRef vs FutureProviderRef),它們各自有不同的屬性(例如 FutureProviderRef 有一個 .future 屬性),這增加了學習成本,而現在所有 ref 都是同一個 Ref 類型,API 更加一致:

V2.0:

// 使用 .autoDispose 修飾符
final myProvider = Provider.autoDispose((ref) {
  return MyObject();
});

V3.0:

// 1. 對於手寫 Provider
final myProvider = Provider(
  (ref) => MyObject(),
  isAutoDispose: true, // 使用 isAutoDispose 參數
);

// 2. 對於程式碼生成的 Provider
@Riverpod(keepAlive: false) // keepAlive: false 是預設行為,等同於 autoDispose
int myProvider(MyProviderRef ref) {
  return 0;
}

類似改動讓 API 更加統一,你不需要再記憶兩套不同的 Provider 名稱,同時職責更清晰,像 ref.stateref.listenSelf 這樣的操作,本質上是與狀態本身的管理相關的,將這些功能移入 Notifier 類,讓 Notifier 成為狀態和其業務邏輯的唯一管理者,而 ref 則專注於依賴注入(閱讀其他 providers)。

例如你需要在一個 Provider 內部監聽自身狀態的變化來執行某些副作用(比如日誌記錄):

V2.0:

final myProvider = FutureProvider<int>((ref) {
  // 使用 ref.listenSelf 監聽自身狀態變化
  ref.listenSelf((previous, next) {
    print('Value changed from $previous to $next');
  });
  return Future.value(0);
});

V3.0:

@riverpod
class MyNotifier extends _MyNotifier {
  @override
  Future<int> build() async {
    // listenSelf 現在是 Notifier 的一個方法
    listenSelf((previous, next) {
      print('Value changed from $previous to $next');
    });
    return 0;
  }
}

可以看到,在 V3.0 中,listenSelf 成為了 MyNotifier 類的一部分,程式碼的組織結構更加清晰,或者假設你想在一個 Provider 內部,每當其狀態更新時,就將新狀態持久化到本地儲存:

V2.0 (使用 ref.listenSelf):

final counterProvider = FutureProvider<int>((ref) async {
  // 在 Provider 內部監聽自身
  ref.listenSelf((previous, next) {
    if (next.hasValue) {
      SharedPreferences.getInstance().then((prefs) {
        prefs.setInt('counter', next.value!);
      });
    }
  });

  // 返回初始值
  final prefs = await SharedPreferences.getInstance();
  return prefs.getInt('counter') ?? 0;
});

V3.0 (使用 Notifier.listenSelf):

@riverpod
class Counter extends _Counter {
  @override
  Future<int> build() async {
    // listenSelf 現在是 Notifier 的一個方法
    listenSelf((previous, next) {
      if (next.hasValue) {
        SharedPreferences.getInstance().then((prefs) {
          prefs.setInt('counter', next.value!);
        });
      }
    });

    final prefs = await SharedPreferences.getInstance();
    return prefs.getInt('counter') ?? 0;
  }

  void increment() async {
    state = AsyncData((state.value ?? 0) + 1);
  }
}

可以看到,在 V3.0 中邏輯更加內聚,Counter 類不僅負責創建狀態,還負責處理與該狀態相關的副作用,程式碼的可讀性和維護性更高。

6、 ProviderObserver 介面變更

ProviderObserver 是一個用於監聽應用中所有 Provider 變化的強大工具,常用於日誌記錄或除錯,在 V3.0 中它的介面發生了變化:

以前 ProviderObserver 的方法會接收 providervaluecontainer 等多個獨立的參數,現在這些參數被統一封裝在一個 ProviderObserverContext 對象。

V2.0:

class MyObserver extends ProviderObserver {
  @override
  void didAddProvider(
    ProviderBase provider,
    Object? value,
    ProviderContainer container,
  ) {
    print('Provider ${provider.name ?? provider.runtimeType} was created');
  }
}

V3.0:

class MyObserver extends ProviderObserver {
  @override
  void didAddProvider(ProviderObserverContext context, Object? value) {
    print('Provider ${context.provider.name ?? context.provider.runtimeType} was created');
  }
}

最後,註解模式並沒有被拋棄,而是得到了進一步加強,如果是在 2.x 版本使用了註解模式,那麼你的遷移成本會更低,例如:

// Before:
@riverpod
Future<int> value(ValueRef ref) async {
  ref.listen(anotherProvider, (previous, next) {
    ref.state++;
  });

  ref.listenSelf((previous, next) {
    print('Log: $previous -> $next');
  });

  ref.future.then((value) {
    print('Future: $value');
  });

  return 0;
}

// After
@riverpod
class Value extends _Value {
  @override
  Future<int> build() async {
    ref.listen(anotherProvider, (previous, next) {
      ref.state++;
    });

    listenSelf((previous, next) {
      print('Log: $previous -> $next');
    });

    future.then((value) {
      print('Future: $value');
    });

    return 0;
  }
}

整體來看, Riverpod 3.0 的重構主要圍繞:

  • 簡化 API ,例如移除 AutoDisposeFamily 的各種變體,統一 Ref 的類型,透過更少的、功能更強大的構建塊來替代大量專用但零散的 API
  • 提升一致性 ,透過統一內部行為,讓對應 Provider 的表現更加可預測,例如統一使用 == 進行更新過濾,確保了無論使用哪種 Provider,對應的重建邏輯都是一致
  • 增強功能 ,在不增加複雜度的前提下,引入如自動重試、離線快取和 Mutation (變更) 支持

那麼,你喜歡 Riverpod 3.0 嗎?

參考連結


原文出處:https://juejin.cn/post/7548643750504333366


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝11   💬6   ❤️10
448
🥈
我愛JS
📝1   💬5   ❤️4
89
🥉
AppleLily
📝1   💬4   ❤️1
50
#4
💬1  
5
#5
xxuan
💬1  
3
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次