时代在进步 Riverpod 作为一个优秀的状态管理,猫哥也开始做些技术调研。今天会写两个例子,计数器、拉取数据。 先说观点,Riverpod 解决了如下几个方面: - 代码比 Provider 简洁,减少嵌套层次 - 通过注解+代码生成加速开发 - 有效解决异步与UI交互

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

@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

插件方式

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()),
        ],
      ),
    );
  }
  @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 学习路径


© 猫哥 ducafecat.com

end