创建你的第一个网络请求provider

创建你的第一个网络请求provider

网络请求是任何一个应用的核心,当作网络请求时需要考虑许多事情:

  • 当网络请求时,UI应该渲染一个加载状态
  • 必须优雅地处理错误
  • 如果有可能,应当缓存请求

在本文中,我们会看到Riverpod是怎么帮助我们自然地处理这些事情的。

建立ProviderScope

启动一个网络请求之前,确保ProviderScope已经添加到应用程序的根部。

less 复制代码
void main(){
    runApp(
      //要安装Riverpod,我们需要添加这个widget在最上层,不是在MyApp里面,而是作为runApp的一个参数
      ProviderScope(
        child:MyApp(),
      ),
    );
}

如此,整个应用程序都可以使用Riverpod了。

备注

如果要完全安装,如安装riverpod_lint和运行code-genreator,参见Getting started

在provider中执行网络请求

我们通常将执行网络请求称之为业务逻辑。在Riverpod中,业务逻辑放在providers中。

provider是一个超级强大的函数,它的使用与普通函数无异,但是有额外的功能:

  • 缓存
  • 提供默认的error/loading处理
  • 可监听
  • 当一些数据发生变化,可自动重新执行

这使得providers完美适用于GET网络请求(也包括POST等请求,参见Performing side effects

举个列子,让我们做一个简单的应用程序,它可以在我们无聊的时候提供随机的活动(Activity)建议。要实现这样一个应用程序,我们需要使用Bored API。特别地,我们将在/api/activity上执行一个GET请求。这个API返回一个JSON对象,我们会将它解析成一个Dart类实例。

下一步将在UI中显示这个活动,当请求进行时,我们会渲染一个loading状态,并优雅地处理错误。

定义Model

在开始之前,需要先定义从API接收到的数据所对应的model,这个model需要将JSON对象解析成Dart类实例。

通常,推荐使用code-generator如Freezedjson_serializable来处理JSON解码,当然你也可以手动处理。

不管怎么样,我们的model如下:

dart 复制代码
class Activity{
  Activity({
    required this.key,
    required this.activity,
    required this.type,
    required this.participants,
    required this.price
  });
  ///转换JSON对象为Activity实例,这会启用读API response的类型安全
  factory Activity.fromJson(Map<String,dynamic> json){
    return Activity(
        key: json['key'] as String, 
        activity: json['activity'] as String, 
        type: json['type'] as String,
        participants:json['participants'] as int,
        price: json['price'] as double,
    );
  }
​
  final String key;
  final String activity;
  final String type;
  final int participants;
  final double price;
}

创建provider

既然有了model,我们可以启动查询API了。首先,我们需要创建第一个provider

定义provider的语法如下所示:

ini 复制代码
final name = SomeProvider.someModifier<Result>((ref){
    <逻辑代码>
});

provider变量(name):用来与我们的provider交互,这个变量必须是final且是全局的(top-level)

provider类型:通常是ProviderFutureProvider或者StreamProvider。Provider的类型取决于你这个函数的返回值。例如,要创建一个Future<Activity>,你可以使用FutureProvider<Activity>

提示

不要想"我应该选择哪个provider",相反,应该想"我想返回什么样的值",这样你就知道如何选择provider了。

Modifiers:有时候,在provider之后会有一个"modifier"。Modifiers是可选的,用来以类型安全的方式微调provider的行为,2种常用的modifiers:

Ref:用来与provider交互的对象,所有的provider都有一个Ref,它或者是provider函数的参数,或者是Notifier的属性。

provider函数:是放置业务逻辑的地方,这个函数在provider第一次被读的时候调用。后续再读这个provider,也不会触发函数的调用,而是返回缓存的值

在我们的例子中,我们想通过API GET 一个activity。GET是异步操作,那意味着我们需要创建一个Future<Activity>

使用前面定义的语法定义provider:

dart 复制代码
  final activityProvider = FutureProvider.autoDispose((ref) async{
    //使用package:http(http: ^1.1.2),从Bored API获取一个随机的activity
    final response = await http.get(Uri.https('boredapi.com','/api.activity'));
    //使用dart:convert, decode JSON playload into a Map data sturcture
    final json = jsonDecode(response.body) as Map<String,dynamic>;
    //最后,将Map转换成Activity实例
    return Activity.fromJson(json);
  });

在这段代码中,我们定义了一个名为activityProvider的provider,UI可以用通过它来获取一个随机的activity,但是它现在还没有任何用处:

  • 网络请求不会执行,直到UI 最近一次read这个provider
  • 后续再read这个provider,不会再触发重新执行网络请求,而是返回先前获取到的activity
  • 如果UI停止使用这个provider,缓存将会被销毁。然后,如果UI再次使用这个provider,会触发新的网络请求
  • 我们没有捕获错误,这是自愿的,因为provider内部处理了错误。如果网络请求或者JSON解析异常,错误信息会被Riverpod捕获。然后UI自动获取需要信息来渲染错误页面。

信息

providers是'Lazy'的,定义一个provider不会执行网络请求,直到它第一次被读。

在UI中渲染网络请求的response

既然已经定义了provider,我们就可以开始使用它来在UI内显示activity了。

与provider交互,我们需要一个名字为ref的对象。在前面provider的定义中可以看到它,所以provider可以访问ref对象。

但那是在provider中,如果是在widget中,该如何获取ref对象?

解决方法是使用一个名为Consumer的自定义widget。Consumer是一个widget,类似于Builder,但是它提供了ref参数,这使得UI可以读provider,下面的例子展示 了如何使用Consumer:

dart 复制代码
import 'dart:convert';
​
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;
class Activity{
  Activity({
    required this.key,
    required this.activity,
    required this.type,
    required this.participants,
    required this.price
  });
  ///转换JSON对象为Activity实例,这会启用读API响应的类型安全
  factory Activity.fromJson(Map<String,dynamic> json){
    return Activity(
        key: json['key'] as String,
        activity: json['activity'] as String,
        type: json['type'] as String,
        participants:json['participants'] as int,
        price: json['price'] as double,
    );
  }
​
  final String key;
  final String activity;
  final String type;
  final int participants;
  final double price;
}
​
final activityProvider = FutureProvider.autoDispose((ref) async{
  //使用package:http(http: ^1.1.2),从Bored API获取一个随机的activity
  final response = await http.get(Uri.https('boredapi.com','/api.activity'));
  //使用dart:convert, decode JSON playload into a Map data sturcture
  final json = jsonDecode(response.body) as Map<String,dynamic>;
  //最后,将Map转换成Activity实例
  return Activity.fromJson(json);
});
​
class Bored extends StatelessWidget{
  const Bored({super.key});
​
​
  @override
  Widget build(BuildContext context) {
    return Consumer(
      builder: (context,ref,child){
        final AsyncValue<Activity> activity = ref.watch(activityProvider);
        return Center(
          child: switch(activity){
            AsyncData(:final value) => Text('Activity: ${value.activity}'),
            AsyncError() => const Text("error"),
            _ => const CircularProgressIndicator()},
        );
      }
    );
  }
}

在这段代码中,我们使用了Consumer来读activityProvider并显示activity,同时我们也处理了加载和错误状态。注意UI是如何不在provider中做额外的事情而完成处理loading/error状态的。

同时,如果这个组件进行重建,网络请求不会重新执行。如果有其他的组件也访问这个activityProvider,网络请求同样不会重新执行。

信息

组件可以通过添加更多的ref.watch调用,来监听多个provider。

深入:使用ConsumerWidget代替Consumer来移除代码缩进

在上面的例子中,我们使用了Consumer来读provider。尽管这种方式没有问题,但是增加的代码缩进让代码更加难以阅读。

Riverpod提供了另外一种方式获取同样的结果:定义一个ConsumerWidget/ConsumerStatefulWidget,代替StatelessWidget/StatefulWidget + Consumer的方式。

ConsumerWidget/ConsumerStatefulWidget是StatelessWidget/StatefulWidget和Consumer的有效融合,他们提供了ref这个额外的好处。我们可以使用ConsumerWidget重写前面的例子:

scala 复制代码
class Bored extends ConsumerWidget{
  const Bored({super.key});
​
​
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final AsyncValue<Activity> activity = ref.watch(activityProvider);
    return Center(
      child: switch(activity){
        AsyncData(:final value) => Text('Activity: ${value.activity}'),
        AsyncError() => const Text("error"),
        _ => const CircularProgressIndicator()},
    );
  }
}

也可以使用ConsumerStatefulWidget来重写:

scss 复制代码
class _MyHomeState extends ConsumerState<MyHome>{
​
  @override
  void initState() {
    super.initState();
    //在State的整个生命周期也可以访问ref,可以监听指定的provider
    ref.listenManual(activityProvider, (previous, next) { 
      //TODO 显示snackbar或dialog
    });
  }
    
  @override
  Widget build(BuildContext context) {
    //ref不再作为参数传递进来,它是ConsumerState的一个属性,这样我们就可以在build中使用ref.watch
    final AsyncValue<Activity> activity = ref.watch(activityProvider);
    return Center(
      child: switch(activity){
        AsyncData(:final value) => Text('Activity: ${value.activity}'),
        AsyncError() => const Text("error"),
        _ => const CircularProgressIndicator()},
    );
  }
 
}
相关推荐
我要最优解7 小时前
关于在mac中配置Java系统环境变量
java·flutter·macos
江上清风山间明月2 天前
Flutter开发的应用页面非常多时如何高效管理路由
android·flutter·路由·页面管理·routes·ongenerateroute
Zsnoin能2 天前
flutter国际化、主题配置、视频播放器UI、扫码功能、水波纹问题
flutter
早起的年轻人2 天前
Flutter CupertinoNavigationBar iOS 风格导航栏的组件
flutter·ios
HappyAcmen2 天前
关于Flutter前端面试题及其答案解析
前端·flutter
coooliang3 天前
Flutter 中的单例模式
javascript·flutter·单例模式
coooliang3 天前
Flutter项目中设置安卓启动页
android·flutter
JIngles1233 天前
flutter将utf-8编码的字节序列转换为中英文字符串
java·javascript·flutter
B.-3 天前
在 Flutter 中实现文件读写
开发语言·学习·flutter·android studio·xcode
freflying11193 天前
使用jenkins构建Android+Flutter项目依赖自动升级带来兼容性问题及Jenkins构建速度慢问题解决
android·flutter·jenkins