Flutter Riverpod 状态管理上手技巧分享
视频
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://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
- vscode
https://marketplace.visualstudio.com/items?itemName=robert-brunhage.flutter-riverpod-snippets - jetbrains https://plugins.jetbrains.com/plugin/14641-flutter-riverpod-snippets
Build Runner
https://marketplace.visualstudio.com/items?itemName=GaetSchwartz.build-runner
例子:计数器
代码
lib/pages/start/index.dart
1 定义代码生成的文件,文件名一直为 index
part 'index.g.dart';
2 注解方式,定义一个 Provider
@riverpod
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 的值
@override
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
插件方式
启动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()),
],
),
);
}
@override
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());
@freezed
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);
}
@freezed
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);
}
@freezed
class Category with _$Category {
const factory Category({
int? id,
String? name,
String? slug,
}) = _Category;
factory Category.fromJson(Map<String, dynamic> json) =>
_$CategoryFromJson(json);
}
@freezed
class Dimensions with _$Dimensions {
const factory Dimensions({
String? length,
String? width,
String? height,
}) = _Dimensions;
factory Dimensions.fromJson(Map<String, dynamic> json) =>
_$DimensionsFromJson(json);
}
@freezed
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);
}
@freezed
class Links with _$Links {
const factory Links({
List<Collection>? self,
List<Collection>? collection,
}) = _Links;
factory Links.fromJson(Map<String, dynamic> json) => _$LinksFromJson(json);
}
@freezed
class Collection with _$Collection {
const factory Collection({
String? href,
}) = _Collection;
factory Collection.fromJson(Map<String, dynamic> json) =>
_$CollectionFromJson(json);
}
@freezed
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';
注解方式 异步请求数据并返回
@riverpod
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 获取数据
@override
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 学习路径
- Flutter 优秀插件推荐 https://flutter.ducafecat.com
- Flutter 基础篇1 - Dart 语言学习 https://ducafecat.com/course/dart-learn
- Flutter 基础篇2 - 快速上手 https://ducafecat.com/course/flutter-quickstart-learn
- Flutter 实战1 - Getx Woo 电商APP https://ducafecat.com/course/flutter-woo
- Flutter 实战2 - 上架指南 Apple Store、Google Play https://ducafecat.com/course/flutter-upload-apple-google
- Flutter 基础篇3 - 仿微信朋友圈 https://ducafecat.com/course/flutter-wechat
- Flutter 实战3 - 腾讯 tim 即时通讯开发 https://ducafecat.com/course/flutter-tim
© 猫哥 ducafecat.com
end