【Flutter】Riverpod中普通Provider与特殊Provider的分类、用法与使用场景

1,320 阅读7分钟

Riverpod中Provider与StreamProvider和FutureProvider的用法

前言

在 Flutter 开发中,状态管理是构建高效和响应式应用的关键。随着应用的复杂性增加,开发者需要一种灵活且强大的方式来管理状态。Riverpod 是一个流行的状态管理库,它以其简洁的 API 和强大的功能,成为了 Flutter 开发者的一个重要工具。关于具体 Riverpod 的介绍和用法我之前已经发布过文章,有兴趣的可以往前翻翻。

在 Riverpod 中,StreamProvider 和 FutureProvider 是处理异步数据流的两种核心组件。StreamProvider 适用于需要持续监听数据流的场景,如实时数据更新和事件监听,而 FutureProvider 则适合处理一次性异步操作,如网络请求和数据加载。这两种提供者不仅简化了异步操作的管理,还提升了应用的响应能力和用户体验。

本文将深入探讨 Riverpod 中 StreamProvider 和 FutureProvider 的用法与使用场景。在此之前我们将首先简要介绍 Provider 的基本定义和使用,然后详细探讨 StreamProvider 和 FutureProvider 的具体应用示例。

一、简单的Provider定义与使用

之前介绍过 Riverpod 的用法,为了引出下面的两种特殊的 Provider 这里简单的过一下普通的 Provider。

class HomeController extends StateNotifier<HomeState> {
  HomeController() : super(HomeState(0));

  void increment() {
    state = HomeState(state.count + 1);
  }
}

// 定义状态提供者
final homeProvider = StateNotifierProvider<HomeController, HomeState>((ref) => HomeController());

你肯定见过这种 Provider 也是目前网上比较多的用法,定义简单使用方便,这也是老版本的用法。

在新版本2.0+中推荐直接使用 Notifier ,默认保留示例,如果是想要在没有引用的时候自动销毁我们可以使用 AutoDisposeNotifier 那么对应的 Provider 就要使用对应的 AutoDisposeNotifierProvider 。

如果不带状态(Notifier)的我们则可以直接返回即可,其中又区分是否带参数(family)。所以整体来说分为一下几种类型:

  1. 不带状态(Notifier)不带参数(family)

  2. 不带状态(Notifier)带参数(family)

  3. 带状态(Notifier)不带参数(family)

  4. 带状态(Notifier)带参数(family)

常规的四种普通的用法,下面是对应的定义示例:

1.1 不带状态(Notifier)不带参数(family)
final myDioProvider = Provider<Dio>((ref) {
  final options = BaseOptions(
    baseUrl: 'https://api.example.com',
    connectTimeout: Duration(seconds: 30),
    receiveTimeout: Duration(seconds: 30),
  );
  return Dio(options);
});

如果是用注解的写法:

part 'your_file_name.g.dart';

@Riverpod(keepAlive: true)  // 或者 @riverpod
Dio dio(DioRef ref) {
  final options = BaseOptions(
    baseUrl: 'https://api.example.com',
    connectTimeout: Duration(seconds: 30),
    receiveTimeout: Duration(seconds: 30),
  );
  return Dio(options);
}

1.2 不带状态(Notifier)带参数(family)
class SettingRepository {
  SettingRepository({required this.dio, required this.api});

  final Dio dio;
  final String api;

  Future<List<String>> fetchSettingConfig({required int type}) async {
    await Future.delayed(const Duration(seconds: 3));
    return Future.value(["1", "2", "3"]);
  }
}

final mySettingRepositoryProvider = Provider.family<SettingRepository, String>((ref, api) {
  final dio = ref.watch(dioProvider);
  return SettingRepository(dio: dio, api: api);
});

//如果要加上 autoDispose 不需配合 Notifier 随便加。
final mySettingRepositoryProvider = Provider.autoDispose.family<SettingRepository, String>((ref, api) {
  final dio = ref.watch(dioProvider);
  return SettingRepository(dio: dio, api: api);
});

如果是用注解的写法:

part 'user_repository.g.dart';

class UserRepository {

  UserRepository({required this.dio});

  final Dio dio;

  Future<String> fetchUserInfo({required String id}) async {
    await Future.delayed(const Duration(seconds: 3));

    return Future.value("Newki");
  }

}

@Riverpod(keepAlive: true)
UserRepository userRepository(UserRepositoryRef ref) => UserRepository(dio: ref.read(dioProvider));

1.3 带状态(Notifier)不带参数(family)
class SettingViewmodel extends AutoDisposeNotifier<SettingState> {
  void fetchUser() async {
    await Future.delayed(const Duration(seconds: 1));

   final settingRepository =  ref.watch(mySettingRepositoryProvider("/setting"));

    state = state.copyWith(userName: "Newki", count: state.count + 1);
  }

  // 重写 build 用于返回初始的状态
  @override
  SettingState build() {
    return SettingState(userName: null, count: 0, linkUrl: null, type: 0);
  }
}

final settingProvider = NotifierProvider.autoDispose<SettingViewmodel, SettingState>(SettingViewmodel.new);

如果要自动销毁要用 AutoDisposeNotifier 否则用 Notifier 和 NotifierProvider

如果是用注解的写法:

@riverpod
class UserViewModel extends _$UserViewModel {
  @override
  UserModel? build() => null;

  Future<void> addUser(String id) async {

    // 将本地缓存标记为脏
    ref.invalidateSelf();
  }

  Future<String> fetchUser(String userId) async {
    await Future.delayed(const Duration(seconds: 2));

    return Future.value("zhang san");
  }

}
1.4 带状态(Notifier)且带参数(family)

错误示范:

class NewsViewModel extends Notifier<SettingState> {
  final String newsId;

  NewsViewModel({required this.newsId});

  @override
  SettingState build() {
    return SettingState(userName: null, count: 0, linkUrl: null, type: 0);
  }
}

// NotifierProvider.autoDispose.family 不能直接传递参数给 Notifier 构造函数 所以这么写报错
final newsProvider = NotifierProvider.autoDispose.family<SettingViewmodel, SettingState, String>((ref, id) {
  return NewsViewModel(newsId: id);
});

不是用在构造函数中的,正确示例:

class NewsViewModel extends AutoDisposeFamilyNotifier<SettingState,String> {
  late final String newsId;

  @override
  SettingState build(String id) {
    newsId = id;
    return SettingState(userName: null, count: 0, linkUrl: null, type: 0);
  }

  void fetchNews() async {
    // 示例异步操作
    await Future.delayed(const Duration(seconds: 1));
    state = state.copyWith(userName: "Newki", count: state.count + 1);
  }
}

final newsProvider = NotifierProvider.autoDispose.family<NewsViewModel, SettingState, String>(NewsViewModel.new);

如果是用注解的写法:

import 'package:riverpod/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'setting_state.dart'; // 假设你有一个 SettingState 类

part 'news_view_model.g.dart'; // 生成的文件

@riverpod
class NewsViewModel extends _$NewsViewModel {
  late final String newsId;

  @override
  SettingState build(String id) {
    newsId = id;
    return SettingState(userName: null, count: 0, linkUrl: null, type: 0);
  }

  void fetchNews() async {
    // 示例异步操作
    await Future.delayed(const Duration(seconds: 1));
    state = state.copyWith(userName: "Newki", count: state.count + 1);
  }
}

简单的分类之后大家需要哪一种类型的 Provider 按需定义即可,如果条件允许的话还是尽量使用注解的方案定义,方便很多!

二、StreamProvider的用法和使用场景

上面我们简单的分类总结了普通 Provider 的定义,我们在平常开发中大部分的场景其实用普通的Provider就已经能覆盖到了,对于一些特殊的场景我们就需要用到 StreamProvider 这种类型。

StreamProvider 是一个特殊的 Provider,用于提供流数据(Stream)。它允许我们在 UI 中实时监听数据更新,适合用于处理动态数据,比如实时聊天、数据监控等场景。

StreamProvider的使用:

定义流:首先定义一个返回 Stream 的函数。

创建 StreamProvider:使用 StreamProvider 来创建流的数据提供者。

使用 Provider:在 UI 中通过 Consumer 或 HookConsumerWidget 来获取和显示数据。

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'dart:async';

// 定义一个返回 Stream 的函数
Stream<int> countStream() async* {
  for (int i = 0; i < 10; i++) {
    await Future.delayed(Duration(seconds: 1));
    yield i; // 发送下一个值
  }
}

// 创建 StreamProvider
final streamProvider = StreamProvider<int>((ref) => countStream());

void main() {
  runApp(
    ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final asyncValue = ref.watch(streamProvider);

    return Scaffold(
      appBar: AppBar(title: Text("StreamProvider 示例")),
      body: Center(
        child: asyncValue.maybeWhen(
          data: (data) => Text('当前值: $data'), // 显示当前流中的值
          loading: () => CircularProgressIndicator(), // 显示加载指示器
          orElse: () => Text('等待数据...'), // 默认显示内容
        ),
      ),
    );
  }
}

每当 stream 生成新值时,UI 会自动更新,展现最新的数据。这种方式非常适合需要实时更新的应用场景,比如社交媒体应用的动态消息、股票价格等。

当然这种流的形式,我们除了直接用 StreamProvider 我们也能使用 Future<Stream> 的方式来获取到 Stream 对象,然后再通过手动的 listen 方式来监听到数据的变化。

例如:

class FavoritesLocalDataSource {
  final Database _database;
  final StoreRef<String, Map<String, dynamic>> _store;

  const FavoritesLocalDataSourceImpl({
    required Database database,
    required StoreRef<String, Map<String, dynamic>> store,
  })  : _database = database,
        _store = store;

  
  Future<void> addBook(Entry book, String id) async {
    await _store.record(id).put(_database, book.toJson());
    Logman.instance.info(
      'Added book to favorites: ${book.title}',
    );
  }


  Future<void> deleteBook(String id) async {
    await _store.record(id).delete(_database);
    Logman.instance.info(
      'Deleted book from favorites: $id',
    );
  }


  Stream<List<Entry>> favoritesListStream() {
    return _store.query().onSnapshots(_database).map<List<Entry>>(
          (records) => records
              .map<Entry>((record) => Entry.fromJson(record.value))
              .toList(),
        );
  }
}

比如我用这个Book的数据类通过 sembast 这个 NOSQL 提供了 Future 和 Stream 的数据来操作数据,然后我们返回对于的 Future<Stream<List<Entry>>> favoritesListStream() 方式:

class FavoritesRepository{
  final FavoritesLocalDataSource localDataSource;

  const FavoritesRepository({
    required this.localDataSource,
  });

  Future<void> addBook(Entry book, String id) async {
    await localDataSource.addBook(book, id);
  }

  Future<void> deleteBook(String id) async {
    await localDataSource.deleteBook(id);
  }

  Future<Stream<List<Entry>>> favoritesListStream() async {
    return localDataSource.favoritesListStream();
  }

  Future<void> clearBooks() async {
    await localDataSource.clearBooks();
  }
}

final favoritesRepositoryProvider = Provider.autoDispose<FavoritesRepository>(
  (ref) {
    final localDataSource = ref.watch(favoritesLocalDataSourceProvider);
    return FavoritesRepository(localDataSource: localDataSource);
  },
);

Callback 一下,这是我们上面讲到的第一种 不带状态(Notifier)不带参数(family) 的用法,同样我们其实也可以用注解的方式去定义,这里不展开说明。

那么在页面或功能的的 Controller 中我们可以定义状态为 List<Entry> 然后获取到对于的 StreamSubscription<List<Entry>> 通过 listen 方式获取到当前最新的数据。

示例如下:

@riverpod
class FavoritesNotifier extends _$FavoritesNotifier {
  late FavoritesRepository _repository;

  FavoritesNotifier() : super();

  StreamSubscription<List<Entry>>? _streamSubscription;

  @override
  Future<List<Entry>> build() async {
    _repository = ref.watch(favoritesRepositoryProvider);

    _listen();
    return [];
  }

  Future<void> _listen() async {
    if (_streamSubscription != null) {
      _streamSubscription!.cancel();
      _streamSubscription = null;
    }
    _streamSubscription = (await _repository.favoritesListStream()).listen(
      (favorites) => state = AsyncValue.data(favorites),
    );
  }

  Future<void> addBook(Entry book, String id) async {
    await _repository.addBook(book, id);
  }

  Future<void> deleteBook(String id) async {
    await _repository.deleteBook(id);
  }

  Future<void> clearBooks() async {
    await _repository.clearBooks();
  }
}

这可以是一个页面的Controller / ViewModel(叫法不同),也可以是一个功能的 UserCase / Service (叫法不同) 这样就是结合普通 Provider + Stream 数据类型实现的类似 StreamProvider 的效果,也是很方便的。

三、FutureProvider的使用场景

那什么是 FutureProvider 呢?

FutureProvider 是用于处理一次性异步操作的 Provider,适合在需要等待某个未来的结果时使用,如网络请求、数据加载等。这种方式可以简化异步操作的状态管理。

在 Riverpod 中,使用 FutureProvider 来处理异步数据时,可以通过以下步骤进行:

定义异步函数:首先定义一个返回 Future 的异步函数。

创建 FutureProvider:使用 FutureProvider 来创建异步数据的提供者。

使用 Provider:在 UI 中通过 Consumer 或 HookConsumerWidget 来获取和显示数据。

// 定义异步函数
Future<String> fetchData() async {
  await Future.delayed(Duration(seconds: 2));
  return "数据加载完成";
}

// 创建 FutureProvider
final futureProvider = FutureProvider<String>((ref) async {
  return await fetchData();
});


class MyHomePage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final asyncValue = ref.watch(futureProvider);

    return Scaffold(
      appBar: AppBar(title: Text("FutureProvider 示例")),
      body: Center(
        child: asyncValue.when(
          data: (data) => Text(data), // 数据加载成功时显示数据
          loading: () => CircularProgressIndicator(), // 数据加载中显示加载指示器
          error: (error, stack) => Text("发生错误: $error"), // 数据加载失败时显示错误信息
        ),
      ),
    );
  }
}

当然了,其实我们和上面一样我们也能通过 Provider + Future 的方式间接的实现 FutureProvider的方式:

我们只需要定义一个 Notifier 中的 build() 方法返回 Future 的类型,在页面中一样可以通过 maybeWhen 或 when 的方式判断 Future 的状态展示不同的状态控件.

@riverpod
class AppThemeNotifier extends _$AppThemeNotifier {
  late AppThemeService _appThemeService;

  AppThemeNotifier() : super();

  Future<void> updateCurrentAppTheme(bool isDarkMode) async {
    final success =
        await _appThemeService.setCurrentAppTheme(isDarkMode);

    if (success) {
      state = AsyncValue.data(
        isDarkMode ? CurrentAppTheme.dark : CurrentAppTheme.light,
      );
    }
  }

  @override
  Future<CurrentAppTheme> build() async {
    _appThemeService = ref.read(appThemeServiceProvider);
    return _appThemeService.getCurrentAppTheme();
  }
}

由于返回的是 Future 对象,我们可以手动的返回 AsyncValue.data() ,那我们接下来就可以直接使用:

判断当前的状态:

 @override
  Widget build(BuildContext context, WidgetRef ref) {
    final currentAppTheme = ref.watch(appThemeNotifierProvider);
    return SwitchListTile(
      secondary: Icon(icon),
      title: Text(title),
      value: currentAppTheme.value == CurrentAppTheme.dark,
      onChanged: (isDarkMode) {
        ref.read(appThemeNotifierProvider.notifier)
            .updateCurrentAppTheme(isDarkMode);
      },
    );
  }

或者根据状态切换布局:

     ref.watch(appThemeNotifierProvider).maybeWhen(
            orElse: () => const SizedBox.shrink(),
            data: (theme) {
              //XXX
            },
          )

与 StreamProvider 的方式类似,可以用 FutureProvider 本身,也可以使用 Provider + Future 的方式都是很方便的。

总结

我们首先了解了普通 Provider 的定义与用法,包括不带状态和带状态的 Provider。这为后续深入理解 StreamProvider 和 FutureProvider 打下了基础。

StreamProvider 适用于需要实时数据更新的场景,如聊天应用、股票监控等。通过示例,我们展示了如何定义流、创建 StreamProvider 并在 UI 中实时监听数据变化。使用 maybeWhen 方法处理不同的状态,使得 UI 可以根据数据的变化动态更新。

与 StreamProvider 不同,FutureProvider 用于处理一次性异步操作,如网络请求和数据加载。通过具体示例,展示了如何定义异步函数、创建 FutureProvider 以及在 UI 中有效管理异步数据的状态,包括数据加载、错误处理等。

在实际应用中,我们可以灵活选择使用 StreamProvider 和 FutureProvider 来直接实现,也可以使用普通的 Provider 结合 Stream 和 Future 对象来使用,注意需要 Provider 的 build 实现是否需要 Future 对象。通过上文中的一些示例我们能理解也能更灵活地处理异步数据和流数据,从而更方便的实现功能逻辑。

OK,那么今天的分享就到这里啦,当然如果你有其他的更多的更好的实现方式,也希望大家能评论区交流一起学习进步。如果我的文章有错别字,不通顺的,或者代码、注释、有错漏的地方,同学们都可以指出修正。

如果感觉本文对你有一点的启发和帮助,还望你能点赞支持一下,你的支持对我真的很重要。

这一期就此完结了。