likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 9

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

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

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

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

建立ProviderScope

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

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如下:

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的语法如下所示:

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:

  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:


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重写前面的例子:

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来重写:

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()},
    );
  }
 
}
转载自:https://juejin.cn/post/7318619321420890152
评论
请登录