Riverpod之FutureProvider(五)

前面介绍几个Provider都是同步,如果是异步的场景怎么办?这时就要上FutureProvider了,它是Provider的异步版本,像一些网络请求场景就非常适合FutureProvider去处理,下面从几个简单Demo看看FutureProvider的使用

读取FutureProvider数据

Demo运行起来后首先看到的是加载指示器转圈2秒,然后显示数字100

void main() {
  runApp(const ProviderScope(child: FutureApp()));
}

class FutureApp extends StatelessWidget {
  const FutureApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: _Home());
  }
}

// 用法和Provider差不多,多了async、await用来处理异步操作
final numberProvider = FutureProvider<int>((ref) async {
  // 模拟耗时操作
  int number = await Future.delayed(const Duration(seconds: 2), () {
    return 100;
  });
  return number;
});

class _Home extends StatelessWidget {
  const _Home();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Consumer(builder: (context, ref, _) {
          debugPrint("build Consumer");
          AsyncValue<int> future = ref.watch(numberProvider);
          return future.when(
              data: (value) {
                return Text('total is  number is $value');
              },
              loading: () => const CircularProgressIndicator(),
              error: (err, stack) => Text("$err"));
        }),
      ),
    );
  }
}

重点看下watch方法,了解一下大致过程,当第一次watch时,要走初始化值过程,FutureProvider内部会判断_create函数的返回值,就是下面这部分

(ref) async {
  int number = await Future.delayed(const Duration(seconds: 2), () {
    return 100;
  });
  return number;
}
  • 是普通类型,那就当Provider使用了,没想到吧,同步的我也能兼容
  • 为Future类型,分两步
    • 1.先将当前状态值设置成AsyncLoading,这样页面就展示了加载指示器,
    • 2.1异步初始化成功,当前状态值设置成AsyncData,页面展示正常数据
    • 2.2异步初始化失败,当前状态值设置成AsyncError,页面展示错误信息。

关于AsyncValue

AsyncValue有如下三个子类

  const factory AsyncValue.data(T value) = AsyncData<T>;

  const factory AsyncValue.loading() = AsyncLoading<T>;

  const factory AsyncValue.error(Object error, {StackTrace? stackTrace}) =
      AsyncError<T>;

每个子类都复写了_map方法,转换对应的参数

AsyncData
  @override
  R _map<R>({
    required R Function(AsyncData<T> data) data,
    required R Function(AsyncError<T> error) error,
    required R Function(AsyncLoading<T> loading) loading,
  }) {
    return data(this);
  }
  
AsyncLoading
  @override
  R _map<R>({
    required R Function(AsyncData<T> data) data,
    required R Function(AsyncError<T> error) error,
    required R Function(AsyncLoading<T> loading) loading,
  }) {
    return loading(this);
  }
 
 AsyncError
  @override
  R _map<R>({
    required R Function(AsyncData<T> data) data,
    required R Function(AsyncError<T> error) error,
    required R Function(AsyncLoading<T> loading) loading,
  }) {
    return error(this);
  }

when方法调用的就是_map方法,这样future是AsyncLoading返回的就是CircularProgressIndicator,future是AsyncData返回的就是TexT

AsyncValue<int> future = ref.watch(numberProvider);
  return future.when(
      data: (value) {
        return Text('total is  number is $value');
      },
      loading: () => const CircularProgressIndicator(),
      error: (err, stack) => Text("$err"));
})

组合多个FutureProvider数据

FutureProvider另一个好用的功能是可以读取其它FutureProvider的状态值,当有多个异步操作,可以整合到一起。我们再添加一个FutureProvider,在读取的时候使用其内部的future,它可以忽略loading状态值,只获取最后状态值,原理看下面小节。最后的效果是加载指示器展示5秒后显示‘300’

final anotherNumberProvider = FutureProvider<int>((ref) async {
  // 模拟耗时操作
  int number = await Future.delayed(const Duration(seconds: 3), () {
    return 200;
  });

  return number;
});

final numberProvider = FutureProvider<int>((ref) async {
  // 模拟耗时操作
  int number = await Future.delayed(const Duration(seconds: 2), () {
    return 100;
  });
  // 使用future将忽略loading状态值,只获取最后状态值
  int anotherNumber = await ref.watch(anotherNumberProvider.future);

  return number + anotherNumber;
});

FutureProvier数据刷新

效果:在屏幕中央显示一个随机数,每次下拉刷新这个数据

// 每次初始化随机生成一个树
Random random = Random();
final randomNumberProvider = FutureProvider<int>((ref) async {
  int number = await Future.delayed(const Duration(seconds: 2), () {
    return random.nextInt(100);
  });
  return number;
});

class FutureApp extends StatelessWidget {
  const FutureApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(home: _Home());
  }
}

class _Home extends StatelessWidget {
  const _Home();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('example')),
      body: Consumer(builder: (context, ref, _) {
        AsyncValue<int> number = ref.watch(randomNumberProvider);
        return number.when(
            data: (value) {
            // 下拉刷新randomNumberProvider数据
              return RefreshIndicator(child: LayoutBuilder(
                builder:
                    (BuildContext context, BoxConstraints viewportConstraints) {
                  return SingleChildScrollView(
                    // child不超视口大小也能scroll,从而可以下拉刷新
                      physics: const AlwaysScrollableScrollPhysics(),
                      child: Container(
                        width: double.infinity,
                        height: viewportConstraints.maxHeight,// 和视口高度一致
                        alignment: Alignment.center,
                        color: Colors.lightBlue,
                        child: Text("${value}"),
                      ));
                },
              ), onRefresh: () async {
                debugPrint("on Refresh");
                // 立马重新初始化provider数据
                return ref.refresh(randomNumberProvider);
              });
            },
            loading: () => const CircularProgressIndicator(),
            error: (err, stack) => Text("$err"));
      }),
    );
  }
}

1.0.4的这个版本,在refresh的时候,立马重新计算,导致先收到loading状态,丢失了之前的页面,如下git图,体验不佳。2.x.x版本有所改善,先使用invalidate让数据失效,再重新read 其future数据,忽略loading过程,如下代码

 onRefresh: () {
      ref.invalidate(randomNumberProvider);
      return ref.read(randomNumberProvider.future);
  },

future_provider.gif

FutureProvider内部流程

前面也提过,其状态的分发分成两部分,

  • 先设置Loading状态
  • 对Future操作添加监听,有结果后更新状态

future_proider.png

代码部分也是很清晰明了

_FutureProviderElementMixin

try {
      final value = future();
       先判断初始化是不是异步操作
      if (value is Future<State>) {
       1.先设置loading状态
        setState(AsyncValue<State>.loading());
       2.监听异步结果
        value.then(
          (event) {
            if (running) setState(AsyncValue<State>.data(event));
          },
          // ignore: avoid_types_on_closure_parameters
          onError: (Object err, StackTrace stack) {
            if (running) {
              setState(
                AsyncValue<State>.error(err, stackTrace: stack),
              );
            }
          },
        );
      } else {
        return AsyncData(value);
      }

      return requireState;
    } catch (err, stack) {
      return AsyncValue.error(err, stackTrace: stack);
    }

FutureProvider的future内部流程

在某些场景下,我们只需要获取FutureProvider的最后结果,而忽略loading状态,这在前面组合多个FutureProvider数据一节有所应用,那这是怎么实现的呢?

这就要用到Completer,这个类非常适合将基于回调的API转换成基于Future的,下面是其用法的伪代码演示

class NetworkHelper {
  final Completer _completer = new Completer();

// 将future返回给客户端,让其等待结果
  Future<T> sendData(String data) {
    client.send(data);
    return _completer.future; 
  }

// 监听后端回应,通知客户端结果
  void _listenFromRemote(bool succeed,String response){
      if(succeed){
          _onSucceed(response);
      }else{
          _onError(response);
      }
  }

  void _onSucceed(String result) {
    _completer.complete(result);
  }

  // If something goes wrong, call this.
  void _onError(error) {
    _completer.completeError(error);
  }
}

NetworkHelper helper=NetworkHelper();
// 发送数据后等待后端回应,阻塞到complete或completeError
String response = await helper.sendData("hello);

流程图画了一下,狗子看了都直摇头,不展示了。future的思路就是,监听FutureProvider的状态,如果是loading,返回_completer.future让客户端等待,等返回data后,使用complete结束等待。需要注意的是Completer不可重复使用,关键代码如下

 void listener(AsyncValue<State>? previous, AsyncValue<State> value) {
    value.when(
      loading: () {
        if (loadingCompleter == null) {
          loadingCompleter = Completer<State>();
          ref.setState(
           调用future的ignore方法后返回future
            loadingCompleter!.future..ignore(),
          );
        }
      },
      data: (data) {
        if (loadingCompleter != null) {
          loadingCompleter!.complete(data);
          // completer不可重复使用
          loadingCompleter = null;
        } else {
          ref.setState(Future<State>.value(data));
        }
      },
      error: (err, stack) {
        if (loadingCompleter != null) {
          loadingCompleter!.completeError(err, stack);
          loadingCompleter = null;
        } else {
          ref.setState(Future<State>.error(err, stack));
        }
      },
    );
  }

参考文档:
https://api.flutter-io.cn/flutter/dart-async/Completer-class.html
https://www.jianshu.com/p/a6516c67f2ce

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值