猫哥课堂 ducafecat.com
开通 VIP 会员, 观看所有视频、附件、猫哥密友、猫哥 VIP 群

Flutter Riverpod 状态管理上手技巧分享

视频

https://youtu.be/6-8H0A2-e3s

https://www.bilibili.com/video/BV1rf421Z7YT/

前言

原文 https://ducafecat.com/blog/flutter-riverpod-state-management-guide-01

时代在进步 Riverpod 作为一个优秀的状态管理,猫哥也开始做些技术调研。今天会写两个例子,计数器、拉取数据。

先说观点,Riverpod 解决了如下几个方面:

  • 代码比 Provider 简洁,减少嵌套层次
  • 通过注解+代码生成加速开发
  • 有效解决异步与UI交互

参考

https://pub.dev/packages/riverpod

https://riverpod.dev/

https://flutter.ducafecat.com/

初始项目

安装插件

flutter pub add flutter_riverpod flutter pub add riverpod_annotation flutter pub add dev:riverpod_generator flutter pub add dev:build_runner flutter pub add dev:custom_lint flutter pub add dev:riverpod_lint

yaml 清单

dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.2 flutter_riverpod: ^2.5.1 riverpod_annotation: ^2.3.5 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.0 riverpod_generator: ^2.4.0 build_runner: ^2.4.8 custom_lint: ^0.6.4

启用 riverpod_lint/custom_lint

pubspec.yaml

analyzer: plugins: - custom_lint

执行检查

dart run custom_lint

IDE 插件

Flutter Riverpod Snippets

flutter riverpod snippets

Build Runner

build runner

https://marketplace.visualstudio.com/items?itemName=GaetSchwartz.build-runner

例子:计数器

代码

lib/pages/start/index.dart

1 定义代码生成的文件,文件名一直为 index

part 'index.g.dart';

2 注解方式,定义一个 Provider

String helloWorld(HelloWorldRef ref) { return 'Hello world'; }

非代码生成方式,不推荐

final helloWorldProvider = Provider((_) => 'Hello world');

3 定义 ConsumerWidget

class StartPage extends ConsumerWidget { const StartPage({super.key});

4 通过 ref 方式获取 Provider 的值

Widget build(BuildContext context, WidgetRef ref) { final String value = ref.watch(helloWorldProvider);
return Scaffold( appBar: AppBar(title: const Text('hello word')), body: Center( child: Text(value), ), );

执行 Build Runner

命令方式

dart run build_runner watch

插件方式

runner bar watch

启动APP

菜单界面

lib/index.dart

Widget _buildBtn(BuildContext context, String title, Widget page) { return ElevatedButton( onPressed: () { Navigator.push( context, MaterialPageRoute(builder: (context) => page), ); }, child: Text(title), ); }
Widget _buildView(BuildContext context) { return Center( child: Column( children: <Widget>[ _buildBtn(context, '01 HelloWord', const StartPage()), ], ), ); }
Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Riverpod 示例')), body: _buildView(context), ); }

ProviderScope 包裹

lib/main.dart

void main() { runApp(const ProviderScope(child: MyApp())); }

例子:拉取数据

本节我们会用到 freezed 一个生成数据实体的工具包。

freezed 使用详解见 https://ducafecat.com/blog/flutter_application_freezed

安装包

命令

# 拉取数据 flutter pub add dio # freezed 生成 flutter pub add freezed_annotation flutter pub add --dev build_runner flutter pub add --dev freezed #(可选)如果你要使用 freezed 来生成 fromJson/toJson,则执行: flutter pub add json_annotation flutter pub add --dev json_serializable

yaml 清单

dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.2 flutter_riverpod: ^2.5.1 riverpod_annotation: ^2.3.5 dio: ^5.4.2 freezed_annotation: ^2.4.1 json_annotation: ^4.8.1 dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.0 riverpod_generator: ^2.4.0 build_runner: ^2.4.8 custom_lint: ^0.6.4 freezed: ^2.4.7 json_serializable: ^6.7.1

数据实例 Entity

数据来源,猫哥 woo 课程商品列表 API https://wpapi.ducafecat.tech/products

在线转换 https://app.quicktype.io/

编写实体类

lib/entity/product_entity.dart

// To parse this JSON data, do // // final productEntity = productEntityFromJson(jsonString); import 'package:freezed_annotation/freezed_annotation.dart'; import 'dart:convert'; part 'product_entity.freezed.dart'; part 'product_entity.g.dart'; ProductEntity productEntityFromJson(String str) => ProductEntity.fromJson(json.decode(str)); String productEntityToJson(ProductEntity data) => json.encode(data.toJson()); class ProductEntity with _$ProductEntity { const factory ProductEntity({ int? id, String? name, String? slug, String? permalink, DateTime? dateCreated, DateTime? dateCreatedGmt, DateTime? dateModified, DateTime? dateModifiedGmt, String? type, String? status, bool? featured, String? catalogVisibility, String? description, String? shortDescription, String? sku, String? price, String? regularPrice, String? salePrice, dynamic dateOnSaleFrom, dynamic dateOnSaleFromGmt, dynamic dateOnSaleTo, dynamic dateOnSaleToGmt, bool? onSale, bool? purchasable, int? totalSales, bool? virtual, bool? downloadable, List<dynamic>? downloads, int? downloadLimit, int? downloadExpiry, String? externalUrl, String? buttonText, String? taxStatus, String? taxClass, bool? manageStock, dynamic stockQuantity, String? backorders, bool? backordersAllowed, bool? backordered, dynamic lowStockAmount, bool? soldIndividually, String? weight, Dimensions? dimensions, bool? shippingRequired, bool? shippingTaxable, String? shippingClass, int? shippingClassId, bool? reviewsAllowed, String? averageRating, int? ratingCount, List<dynamic>? upsellIds, List<dynamic>? crossSellIds, int? parentId, String? purchaseNote, List<Category>? categories, List<Category>? tags, List<Image>? images, List<Attribute>? attributes, List<dynamic>? defaultAttributes, List<dynamic>? variations, List<dynamic>? groupedProducts, int? menuOrder, String? priceHtml, List<int>? relatedIds, List<MetaDatum>? metaData, String? stockStatus, bool? hasOptions, Links? links, }) = _ProductEntity; factory ProductEntity.fromJson(Map<String, dynamic> json) => _$ProductEntityFromJson(json); } class Attribute with _$Attribute { const factory Attribute({ int? id, String? name, int? position, bool? visible, bool? variation, List<String>? options, }) = _Attribute; factory Attribute.fromJson(Map<String, dynamic> json) => _$AttributeFromJson(json); } class Category with _$Category { const factory Category({ int? id, String? name, String? slug, }) = _Category; factory Category.fromJson(Map<String, dynamic> json) => _$CategoryFromJson(json); } class Dimensions with _$Dimensions { const factory Dimensions({ String? length, String? width, String? height, }) = _Dimensions; factory Dimensions.fromJson(Map<String, dynamic> json) => _$DimensionsFromJson(json); } class Image with _$Image { const factory Image({ int? id, DateTime? dateCreated, DateTime? dateCreatedGmt, DateTime? dateModified, DateTime? dateModifiedGmt, String? src, String? name, String? alt, }) = _Image; factory Image.fromJson(Map<String, dynamic> json) => _$ImageFromJson(json); } class Links with _$Links { const factory Links({ List<Collection>? self, List<Collection>? collection, }) = _Links; factory Links.fromJson(Map<String, dynamic> json) => _$LinksFromJson(json); } class Collection with _$Collection { const factory Collection({ String? href, }) = _Collection; factory Collection.fromJson(Map<String, dynamic> json) => _$CollectionFromJson(json); } class MetaDatum with _$MetaDatum { const factory MetaDatum({ int? id, String? key, String? value, }) = _MetaDatum; factory MetaDatum.fromJson(Map<String, dynamic> json) => _$MetaDatumFromJson(json); }

执行编译,工具或命令,我使用插件方式

dart run build_runner watch

定义 provider

lib/provider/products.dart

定义生成代码的文件

part 'products.g.dart';

注解方式 异步请求数据并返回

Future<List<ProductEntity>> products(ProductsRef ref) async { String url = "https://wpapi.ducafecat.tech/products"; Response response = await Dio().get(url); List<ProductEntity> list = []; for (var item in response.data) { list.add(ProductEntity.fromJson(item)); } return list; }

业务界面

lib/pages/network/index.dart

ConsumerWidget 方式

// 1 ConsumerWidget 方式 class NetworkPage extends ConsumerWidget { const NetworkPage({super.key});

通过 ref.watch 获取数据

Widget build(BuildContext context, WidgetRef ref) { // 2 通过 ref.watch 获取数据 final AsyncValue<List<ProductEntity>> products = ref.watch(productsProvider); return Scaffold( appBar: AppBar( title: const Text('拉取数据'), ), body: _buildView(products), ); }

构建视图

// 3 构建视图 Widget _buildView(AsyncValue<List<ProductEntity>> products) { return Center( child: switch (products) { // 4 根据状态显示不同的视图 AsyncData<List<ProductEntity>>(:final value) => ListView.builder( itemCount: value.length, itemBuilder: (context, index) { return ListTile( title: Text(value[index].name ?? ""), subtitle: Text(value[index].description ?? ""), ); }, ), // 5 错误处理 AsyncError() => const Text('Oops, something unexpected happened'), // 6 加载中 _ => const CircularProgressIndicator(), }, ); }

启动菜单

lib/pages/index.dart

Widget _buildView(BuildContext context) { return Center( child: Column( children: <Widget>[ _buildBtn(context, '01 HelloWord', const StartPage()), _buildBtn(context, '02 网络请求', const NetworkPage()), ], ), ); }

代码

https://github.com/ducafecat/flutter_develop_tips/tree/main/flutter_application_riverpod

小结

Riverpod通过声明式和反应式编程风格,为开发者处理应用程序的逻辑提供了全新方式。最后说几点猫哥个人建议:

  • 如果可能不要用 flutter_hooks 不是必须项
  • 推荐用注解+代码生成方式(时代在进步,阅读、扩展、维护)
  • 使用 ConsumerWidget 简化代码
  • 使用插件生成代码

感谢阅读本文

如果有什么建议,请在评论中让我知道。我很乐意改进。


flutter 学习路径


© 猫哥 ducafecat.com

end


Copyright 2024 ducafecat. All rights reserved.
微信: ducafecat, line: ducafecat,京ICP备2021009050号-3