创建你的第一个网络请求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如Freezed或json_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类型:通常是Provider
,FutureProvider
或者StreamProvider
。Provider的类型取决于你这个函数的返回值。例如,要创建一个Future<Activity>
,你可以使用FutureProvider<Activity>
。
提示
不要想"我应该选择哪个provider",相反,应该想"我想返回什么样的值",这样你就知道如何选择provider了。
Modifiers:有时候,在provider之后会有一个"modifier"。Modifiers是可选的,用来以类型安全的方式微调provider的行为,2种常用的modifiers:
autoDispose
,当provider停止使用的时候,自动清除缓存,参见Clearing cache and reacting to state disposalfamily
,向provider传递参数,参见Passing arguments to your requests。
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()},
);
}
}