1. 重走Flutter狀態(tài)管理之路—Riverpod進階篇

        共 27450字,需瀏覽 55分鐘

         ·

        2022-05-24 12:17

        點擊上方藍字關(guān)注我,知識會給你力量


        前面一篇文章,我們了解了如何正確的去讀取狀態(tài)值,這一篇,我們來了解下不同的Provider都有哪些使用場景。這篇文章,我們將真正的深入了解,如何在不同的場景下,選擇合適的種類的Provider,以及這些不同類型的Provider,都有哪些作用。

        不同類型的Provider

        Provider有多種類型的變種,可以用于多種不同的使用場景。

        在所有這些Provider中,有時很難理解何時使用一種Provider類型而不是另一種。使用下面的表格,選擇一個適合你想提供給Widget樹的Provider。

        Provider TypeProvider Create FunctionExample Use Case
        ProviderReturns any typeA service class / computed property (filtered list)
        StateProviderReturns any typeA filter condition / simple state object
        FutureProviderReturns a Future of any typeA result from an API call
        StreamProviderReturns a Stream of any typeA stream of results from an API
        StateNotifierProviderReturns a subclass of StateNotifierA complex state object that is immutable except through an interface
        ChangeNotifierProviderReturns a subclass of ChangeNotifierA complex state object that requires mutability

        雖然所有的Provider都有他們的目的,但ChangeNotifierProviders不被推薦用于可擴展的應(yīng)用程序,因為它存在可變的狀態(tài)問題。它存在于flutter_riverpod包中,以提供一個簡單的從package:provider的遷移組件,并允許一些flutter特定的使用情況,如與一些Navigator 2包的集成。

        Provider

        Provider是所有Providers中最基本的。它返回了一個Value... 僅此而已。

        Provider通常用于下面的場景。

        • 緩存計算后的值
        • 將一個值暴露給其他Provider(比如Repository/HttpClient)
        • 提供了一個可供測試的覆寫Provider
        • 通過不使用select,來減少Provider/widget的重建

        通過Provider來對計算值進行緩存

        當與ref.watch結(jié)合時,Provider是一個強大的工具,用于緩存同步操作。

        一個典型的例子是過濾一個todos的列表。由于過濾一個列表的成本較高,我們最好不要在我們的應(yīng)用程序每次需要重新渲染的時候,就過濾一次我們的todos列表。在這種情況下,我們可以使用Provider來為我們做過濾工作。

        為此,假設(shè)我們的應(yīng)用程序有一個現(xiàn)有的StateNotifierProvider,它管理一個todos列表。

        class Todo {
          Todo(this.description, this.isCompleted);
          final bool isCompleted;
          final String description;
        }

        class TodosNotifier extends StateNotifier<List<Todo>> {
          TodosNotifier() : super([]);

          void addTodo(Todo todo) {
            state = [...state, todo];
          }
          // TODO add other methods, such as "removeTodo", ...
        }

        final todosProvider = StateNotifierProvider<TodosNotifier, List<Todo>>((ref) {
          return TodosNotifier();
        });

        在這里,我們可以使用Provider來管理一個過濾后的todos列表,只顯示已完成的todos。

        final completedTodosProvider = Provider<List<Todo>>((ref) {
          // We obtain the list of all todos from the todosProvider
          final todos = ref.watch(todosProvider);

          // we return only the completed todos
          return todos.where((todo) => todo.isCompleted).toList();
        });

        有了這段代碼,我們的用戶界面現(xiàn)在能夠通過監(jiān)聽 completedTodosProvider來顯示已完成的todos列表。

        Consumer(builder: (context, ref, child) {
          final completedTodos = ref.watch(completedTodosProvider);
          // TODO show the todos using a ListView/GridView/...
        });

        有趣的是,現(xiàn)在的過濾后的列表是被緩存的。這意味著在添加/刪除/更新todos之前,已完成的todos列表不會被重新計算,即使我們多次讀取已完成的todos列表。

        請注意,當todos列表發(fā)生變化時,我們不需要手動使緩存失效。由于有了ref.watch,Provider能夠自動知道何時必須重新計算結(jié)果。

        通過Provider來減少provider/widget的重建

        Provider的一個獨特之處在于,即使Provider被重新計算(通常在使用ref.watch時),它也不會更新監(jiān)聽它的widgets/providers,除非其值發(fā)生了變化。

        一個真實的例子是啟用/禁用一個分頁視圖的上一個/下一個按鈕。

        stepper example

        在我們的案例中,我們將特別關(guān)注 "上一頁 "按鈕。這種按鈕的一個普通的實現(xiàn),是一個獲得當前頁面索引的Widget,如果該索引等于0,我們將禁用該按鈕。

        這段代碼可以是這樣。

        final pageIndexProvider = StateProvider<int>((ref) => 0);

        class PreviousButton extends ConsumerWidget {
          const PreviousButton({Key? key}): super(key: key);

          @override
          Widget build(BuildContext context, WidgetRef ref) {
            // if not on first page, the previous button is active
            final canGoToPreviousPage = ref.watch(pageIndexProvider) != 0;

            void goToPreviousPage() {
              ref.read(pageIndexProvider.notifier).update((state) => state - 1);
            }

            return ElevatedButton(
              onPressed: canGoToPreviousPage ? null : goToPreviousPage,
              child: const Text('previous'),
            );
          }
        }

        這段代碼的問題是,每當我們改變當前頁面時,"上一頁 "按鈕就會重新Build。在理想的世界里,我們希望這個按鈕只在激活和停用之間變化時才重新build。

        這里問題的根源在于,我們正在計算用戶是否被允許在 "上一頁 "按鈕中直接轉(zhuǎn)到上一頁。

        解決這個問題的方法是把這個邏輯從widget中提取出來,放到一個Provider中。

        final pageIndexProvider = StateProvider<int>((ref) => 0);

        // A provider which computes whether the user is allowed to go to the previous page
        final canGoToPreviousPageProvider = Provider<bool>((ref) {
          return ref.watch(pageIndexProvider) != 0;
        });

        class PreviousButton extends ConsumerWidget {
          const PreviousButton({Key? key}): super(key: key);

          @override
          Widget build(BuildContext context, WidgetRef ref) {
            // We are now watching our new Provider
            // Our widget is no longer calculating whether we can go to the previous page.
            final canGoToPreviousPage = ref.watch(canGoToPreviousPageProvider);

            void goToPreviousPage() {
              ref.read(pageIndexProvider.notifier).update((state) => state - 1);
            }

            return ElevatedButton(
              onPressed: canGoToPreviousPage ? null : goToPreviousPage,
              child: const Text('previous'),
            );
          }
        }

        通過這個小的重構(gòu),我們的PreviousButton Widget將不會在頁面索引改變時重建,這都要歸功于Provider的緩存作用。

        從現(xiàn)在開始,當頁面索引改變時,我們的canGoToPreviousPageProviderProvider將被重新計算。但是如果Provider暴露的值沒有變化,那么PreviousButton將不會重建。

        這個變化既提高了我們的按鈕的性能,又有一個有趣的好處,就是把邏輯提取到我們的Widget之外。

        StateProvider

        我們再來看下StateProvider,它是一個公開了修改其狀態(tài)的方法的Provider。它是StateNotifierProvider的簡化版,旨在避免為非常簡單的用例編寫一個StateNotifier類。

        StateProvider的存在主要是為了允許用戶對簡單的變量進行修改。一個StateProvider所維護的狀態(tài)通常是下面幾種。

        • 一個枚舉,比如一個filter,用來做篩選
        • 一個字符串,通常是一些固定的文本,可以借助family關(guān)鍵字來做Switch
        • 一個布爾值,用于checkbox這類的狀態(tài)切換
        • 一個數(shù)字,用于分頁或者Pager的Index

        而下面這些場景,就不適合使用StateProvider。

        • 你的狀態(tài)中包含對校驗邏輯
        • 你的狀態(tài)是一個復(fù)雜的對象,比如一個自定義類,一個List、Map等
        • 狀態(tài)的修改邏輯比較復(fù)雜

        對于這些場景,你可以考慮使用StateNotifierProvider代替,并創(chuàng)建一個StateNotifier類。

        雖然StateNotifierProvider的模板代碼會多一些,但擁有一個自定義的StateNotifier類對于項目的長期可維護性至關(guān)重要--因為它將你的狀態(tài)的業(yè)務(wù)邏輯集中在一個地方。

        由此,我們可以了解,Riverpod最合適的場景,就是「單一狀態(tài)值的管理」。例如,PageView的切換Index、ListView的切換Index,或者是CheckBox、dropdown的內(nèi)容改變監(jiān)聽,這些是非常適合用StateProvider的。

        一個filter的示例

        官方給出了一個dropdown的例子,用來演示如何根據(jù)filter來修改列表的排序。

        StateProvider在現(xiàn)實世界中的一個使用案例是管理簡單表單組件的狀態(tài),如dropdown/text fields/checkboxes。特別是,我們將看到如何使用StateProvider來實現(xiàn)一個允許改變產(chǎn)品列表排序方式的dropdown。為了簡單起見,我們將獲得的產(chǎn)品列表將直接在應(yīng)用程序中建立,其內(nèi)容如下。

        class Product {
          Product({required this.name, required this.price});

          final String name;
          final double price;
        }

        final _products = [
          Product(name: 'iPhone', price: 999),
          Product(name: 'cookie', price: 2),
          Product(name: 'ps5', price: 500),
        ];

        final productsProvider = Provider<List<Product>>((ref) {
          return _products;
        });

        在現(xiàn)實世界的應(yīng)用中,這個列表通常是通過使用FutureProvider進行網(wǎng)絡(luò)請求來獲得的,然后,用戶界面可以顯示產(chǎn)品列表,就像下面這樣。

        Widget build(BuildContext context, WidgetRef ref) {
          final products = ref.watch(productsProvider);
          return Scaffold(
            body: ListView.builder(
              itemCount: products.length,
              itemBuilder: (context, index) {
                final product = products[index];
                return ListTile(
                  title: Text(product.name),
                  subtitle: Text('${product.price} \$'),
                );
              },
            ),
          );
        }

        由于這里是寫死了products,所以使用Provider來作為數(shù)據(jù)Provider,是一個很好的選擇。

        現(xiàn)在我們已經(jīng)完成了基礎(chǔ)框架,我們可以添加一個dropdown,這將允許我們通過價格或名稱來過濾產(chǎn)品。為此,我們將使用DropDownButton。

        // An enum representing the filter type
        enum ProductSortType {
          name,
          price,
        }

        Widget build(BuildContext context, WidgetRef ref) {
          final products = ref.watch(productsProvider);
          return Scaffold(
            appBar: AppBar(
              title: const Text('Products'),
              actions: [
                DropdownButton<ProductSortType>(
                  value: ProductSortType.price,
                  onChanged: (value) {},
                  items: const [
                    DropdownMenuItem(
                      value: ProductSortType.name,
                      child: Icon(Icons.sort_by_alpha),
                    ),
                    DropdownMenuItem(
                      value: ProductSortType.price,
                      child: Icon(Icons.sort),
                    ),
                  ],
                ),
              ],
            ),
            body: ListView.builder(
              // ... 
            ),
          );
        }

        現(xiàn)在我們有了一個dropdown,讓我們創(chuàng)建一個StateProvider并將dropdown的狀態(tài)與我們的StateProvider同步。首先,讓我們創(chuàng)建StateProvider。

        final productSortTypeProvider = StateProvider<ProductSortType>(
          // We return the default sort type, here name.
          (ref) => ProductSortType.name,
        );

        然后我們可以通過下面這個方式,將StateProvider和dropdown聯(lián)系起來。

        DropdownButton<ProductSortType>(
          // When the sort type changes, this will rebuild the dropdown
          // to update the icon shown.
          value: ref.watch(productSortTypeProvider),
          // When the user interacts with the dropdown, we update the provider state.
          onChanged: (value) =>
              ref.read(productSortTypeProvider.notifier).state = value!,
          items: [
            // ...
          ],
        ),

        有了這個,我們現(xiàn)在應(yīng)該能夠改變排序類型。不過,這對產(chǎn)品列表還沒有影響。現(xiàn)在是最后一個部分了。更新我們的productsProvider來對產(chǎn)品列表進行排序。

        實現(xiàn)這一點的一個關(guān)鍵部分是使用ref.watch,讓我們的productProvider獲取排序類型,并在排序類型改變時重新計算產(chǎn)品列表。實現(xiàn)的方法如下。

        final productsProvider = Provider<List<Product>>((ref) {
          final sortType = ref.watch(productSortTypeProvider);
          switch (sortType) {
            case ProductSortType.name:
              return _products.sorted((a, b) => a.name.compareTo(b.name));
            case ProductSortType.price:
              return _products.sorted((a, b) => a.price.compareTo(b.price));
          }
        });

        這就是全部代碼,這一改變足以讓用戶界面在排序類型改變時自動重新對產(chǎn)品列表進行排序。

        更新狀態(tài)的簡化

        參考下面的這個場景,有時候,我們需要根據(jù)前一個狀態(tài)值,來修改后續(xù)的狀態(tài)值,例如Flutter Demo中的加數(shù)器。

        final counterProvider = StateProvider<int>((ref) => 0);

        class HomeView extends ConsumerWidget {
          const HomeView({Key? key}): super(key: key);

          @override
          Widget build(BuildContext context, WidgetRef ref) {
            return Scaffold(
              floatingActionButton: FloatingActionButton(
                onPressed: () {
                  // We're updating the state from the previous value, we ended-up reading
                  // the provider twice!
                  ref.read(counterProvider.notifier).state = ref.read(counterProvider.notifier).state + 1;
                },
              ),
            );
          }
        }

        這種更新State的方法,我們可以使用update函數(shù)來簡化,簡化之后,代碼如下。

        ref.read(counterProvider.notifier).update((state) => state + 1);

        所以,如果是對StateProvider的state進行賦值,那么直接使用下面的代碼即可。

        ref.read(counterProvider.notifier).state = xxxx

        那么如果是根據(jù)前置狀態(tài)的值來修改狀態(tài)值,則可以使用update來簡化。

        StateNotifierProvider

        StateNotifierProvider是一個用于監(jiān)聽和管理StateNotifier的Provider。StateNotifierProvider和StateNotifier是Riverpod推薦的解決方案,用于管理可能因用戶交互而改變的狀態(tài)。

        它通常被用于下面這些場景。

        • 暴露一個不可變的,跟隨時間和行為而發(fā)生改變的狀態(tài)
        • 將修改某些狀態(tài)的邏輯(又稱 "業(yè)務(wù)邏輯")集中在一個地方,提高長期的可維護性

        作為一個使用例子,我們可以使用StateNotifierProvider來實現(xiàn)一個todo-list。這樣做可以讓我們暴露出諸如addTodo這樣的方法,讓UI在用戶交互中修改todos列表。

        // The state of our StateNotifier should be immutable.
        // We could also use packages like Freezed to help with the implementation.
        @immutable
        class Todo {
          const Todo({required this.id, required this.description, required this.completed});

          // All properties should be `final` on our class.
          final String id;
          final String description;
          final bool completed;

          // Since Todo is immutable, we implement a method that allows cloning the
          // Todo with slightly different content.
          Todo copyWith({String? id, String? description, bool? completed}) {
            return Todo(
              id: id ?? this.id,
              description: description ?? this.description,
              completed: completed ?? this.completed,
            );
          }
        }

        // The StateNotifier class that will be passed to our StateNotifierProvider.
        // This class should not expose state outside of its "state" property, which means
        // no public getters/properties!
        // The public methods on this class will be what allow the UI to modify the state.
        class TodosNotifier extends StateNotifier<List<Todo>> {
          // We initialize the list of todos to an empty list
          TodosNotifier(): super([]);

          // Let's allow the UI to add todos.
          void addTodo(Todo todo) {
            // Since our state is immutable, we are not allowed to do `state.add(todo)`.
            // Instead, we should create a new list of todos which contains the previous
            // items and the new one.
            // Using Dart'
        s spread operator here is helpful!
            state = [...state, todo];
            // No need to call "notifyListeners" or anything similar. Calling "state ="
            // will automatically rebuild the UI when necessary.
          }

          // Let's allow removing todos
          void removeTodo(String todoId) {
            // Again, our state is immutable. So we'
        re making a new list instead of
            // changing the existing list.
            state = [
              for (final todo in state)
                if (todo.id != todoId) todo,
            ];
          }

          // Let's mark a todo as completed
          void toggle(String todoId) {
            state = [
              for (final todo in state)
                // we'
        re marking only the matching todo as completed
                if (todo.id == todoId)
                  // Once more, since our state is immutable, we need to make a copy
                  // of the todo. We're using our `copyWith` method implemented before
                  // to help with that.
                  todo.copyWith(completed: !todo.completed)
                else
                  // other todos are not modified
                  todo,
            ];
          }
        }

        // Finally, we are using StateNotifierProvider to allow the UI to interact with
        // our TodosNotifier class.
        final todosProvider = StateNotifierProvider<TodosNotifier, List<Todo>>((ref) {
          return TodosNotifier();
        });

        現(xiàn)在我們已經(jīng)定義了一個StateNotifierProvider,我們可以用它來與用戶界面中的todos列表進行交互。

        class TodoListView extends ConsumerWidget {
          const TodoListView({Key? key}): super(key: key);

          @override
          Widget build(BuildContext context, WidgetRef ref) {
            // rebuild the widget when the todo list changes
            List<Todo> todos = ref.watch(todosProvider);

            // Let's render the todos in a scrollable list view
            return ListView(
              children: [
                for (final todo in todos)
                  CheckboxListTile(
                    value: todo.completed,
                    // When tapping on the todo, change its completed status
                    onChanged: (value) => ref.read(todosProvider.notifier).toggle(todo.id),
                    title: Text(todo.description),
                  ),
              ],
            );
          }
        }

        FutureProvider

        FutureProvider相當于Provider,但僅用于異步代碼。

        FutureProvider通常用于下面這些場景。

        • 執(zhí)行和緩存異步操作(如網(wǎng)絡(luò)請求)
        • 更好地處理異步操作的錯誤、加載狀態(tài)
        • 將多個異步值合并為另一個值

        FutureProvider在與ref.watch結(jié)合時收獲頗豐。這種組合允許在一些變量發(fā)生變化時自動重新獲取一些數(shù)據(jù),確保我們始終擁有最新的值。

        ?

        FutureProvider不提供在用戶交互后直接修改計算的方法。它被設(shè)計用來解決簡單的用例。

        對于更高級的場景,可以考慮使用StateNotifierProvider。

        ?

        示例:讀取一個配置文件

        FutureProvider可以作為一種方便的方式來管理一個通過讀取JSON文件創(chuàng)建的配置對象。

        創(chuàng)建配置將用典型的async/await語法完成,但在Provider內(nèi)部。使用Flutter的asset,這將是下面的代碼。

        final configProvider = FutureProvider<Configuration>((ref) async {
          final content = json.decode(
            await rootBundle.loadString('assets/configurations.json'),
          ) as Map<String, Object?>;

          return Configuration.fromJson(content);
        });

        然后,用戶界面可以像這樣監(jiān)聽配置。

        Widget build(BuildContext context, WidgetRef ref) {
          AsyncValue<Configuration> config = ref.watch(configProvider);

          return config.when(
            loading: () => const CircularProgressIndicator(),
            error: (err, stack) => Text('Error: $err'),
            data: (config) {
              return Text(config.host);
            },
          );
        }

        這將在Future完成后自動重建UI。同時,如果多個widget想要這些解析值,asset將只被解碼一次。

        正如你所看到的,監(jiān)聽Widget內(nèi)的FutureProvider會返回一個AsyncValue - 它允許處理錯誤/加載狀態(tài)。

        StreamProvider

        StreamProvider類似于FutureProvider,但用于Stream而不是Future。

        StreamProvider通常被用于下面這些場景。

        • 監(jiān)聽Firebase或web-sockets
        • 每隔幾秒鐘重建另一個Provider

        由于Streams自然地暴露了一種監(jiān)聽更新的方式,有些人可能認為使用StreamProvider的價值很低。特別是,你可能認為Flutter的StreamBuilder也能很好地用于監(jiān)聽Stream,但這是一個錯誤。

        使用StreamProvider而不是StreamBuilder有許多好處。

        • 它允許其他Provider使用ref.watch來監(jiān)聽Stream
        • 由于AsyncValue的存在,它可以確保加載和錯誤情況得到正確處理
        • 它消除了區(qū)分broadcast streams和normal stream的需要
        • 它緩存了stream所發(fā)出的最新值,確保如果在事件發(fā)出后添加了監(jiān)聽器,監(jiān)聽器仍然可以立即訪問最新的事件
        • 它允許在測試中通過覆蓋StreamProvider的方式來mock stream

        ChangeNotifierProvider

        ChangeNotifierProvider是一個用來管理Flutter中的ChangeNotifier的Provider。

        Riverpod不鼓勵使用ChangeNotifierProvider,它的存在主要是為了下面這些場景。

        • 從package:provider的代碼遷移到Riverpod時,替代原有的ChangeNotifierProvider
        • 支持可變的狀態(tài)管理,但是,不可變的狀態(tài)是首選推薦的
        ?

        更傾向于使用StateNotifierProvider來代替。

        只有當你絕對確定你想要可變的狀態(tài)時,才考慮使用ChangeNotifierProvider。

        ?

        使用可變的狀態(tài)而不是不可變的狀態(tài)有時會更有效率。但缺點是,它可能更難維護,并可能破壞各種功能。

        例如,如果你的狀態(tài)是可變的,使用provider.select來優(yōu)化Widget的重建可能就會失效,因為select會認為值沒有變化。

        因此,使用不可變的數(shù)據(jù)結(jié)構(gòu)有時會更快。而且,針對你的用例進行基準測試很重要,以確保你通過使用ChangeNotifierProvider真正獲得了性能。

        作為一個使用例子,我們可以使用ChangeNotifierProvider來實現(xiàn)一個todo-list。這樣做將允許我們公開諸如addTodo的方法,讓UI在用戶交互中修改todos列表。

        class Todo {
          Todo({
            required this.id,
            required this.description,
            required this.completed,
          });

          String id;
          String description;
          bool completed;
        }

        class TodosNotifier extends ChangeNotifier {
          final todos = <Todo>[];

          // Let's allow the UI to add todos.
          void addTodo(Todo todo) {
            todos.add(todo);
            notifyListeners();
          }

          // Let'
        s allow removing todos
          void removeTodo(String todoId) {
            todos.remove(todos.firstWhere((element) => element.id == todoId));
            notifyListeners();
          }

          // Let's mark a todo as completed
          void toggle(String todoId) {
            for (final todo in todos) {
              if (todo.id == todoId) {
                todo.completed = !todo.completed;
                notifyListeners();
              }
            }
          }
        }

        // Finally, we are using StateNotifierProvider to allow the UI to interact with
        // our TodosNotifier class.
        final todosProvider = ChangeNotifierProvider<TodosNotifier>((ref) {
          return TodosNotifier();
        });

        現(xiàn)在我們已經(jīng)定義了一個ChangeNotifierProvider,我們可以用它來與用戶界面中的todos列表進行交互。

        class TodoListView extends ConsumerWidget {
          const TodoListView({Key? key}) : super(key: key);

          @override
          Widget build(BuildContext context, WidgetRef ref) {
            // rebuild the widget when the todo list changes
            List<Todo> todos = ref.watch(todosProvider).todos;

            // Let's render the todos in a scrollable list view
            return ListView(
              children: [
                for (final todo in todos)
                  CheckboxListTile(
                    value: todo.completed,
                    // When tapping on the todo, change its completed status
                    onChanged: (value) =>
                        ref.read(todosProvider.notifier).toggle(todo.id),
                    title: Text(todo.description),
                  ),
              ],
            );
          }
        }

        這些不同類型的各種Provider,就是我們的軍火庫,我們需要根據(jù)不同的場景和它們的特性來選擇不同的「武器」,通過文中給出的例子,相信大家能夠很好的理解它們的作用了。

        向大家推薦下我的網(wǎng)站 https://xuyisheng.top/  點擊原文一鍵直達

        專注 Android-Kotlin-Flutter 歡迎大家訪問



        往期推薦


        本文原創(chuàng)公眾號:群英傳,授權(quán)轉(zhuǎn)載請聯(lián)系微信(Tomcat_xu),授權(quán)后,請在原創(chuàng)發(fā)表24小時后轉(zhuǎn)載。
        < END >
        作者:徐宜生

        更文不易,點個“三連”支持一下??


        瀏覽 58
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
          
          

            1. 红桃视频国产在线 | 免费在线观看黄色片 | 久久久久亚洲AV成人人人 | 美女尿口网站 | 午夜精品久久一牛影视 |