如何在 Flutter 应用程序中开始使用 Riverpod、StateNotifier 和 Freezed。
如何结合使用这 3 个工具来管理应用程序的状态。
Haz clic aquí para la 版本 en español
我们越来越多地听说Riverpod
Flutter 中有一个全新的状态管理解决方案。最大的问题是,尝试使用它的人甚至不知道从哪里开始。
在本文中,我将重点介绍如何使用Riverpod、StateNotifer和Freezed一起构建项目以创建完整的状态管理流程。
我们要建造什么?
我们将构建一个笑话应用程序,它将使用Jokes API获取一个新的编程笑话,然后将其显示在屏幕上。结果将如下所示:
基础知识
我可以对每个工具都进行解释,但没必要重新发明轮子。我的朋友Marcos Sevilla有一些很棒的文章来解释这些工具。
- Riverpod 文章
- 该工具允许我们注入依赖项并封装状态,然后监听该状态。
- StateNotifier 文章
- 它是 ValueNotifier 的“重新实现”,不同之处在于它不依赖于 Flutter。
- 冷冻物品
- 它是不可变类的代码生成器。
现在您已经了解了这些工具各自是什么以及它们如何工作,让我们看看如何将它们一起使用作为状态管理解决方案。
⚠️ 在继续之前,了解工具的基本概念非常重要,因此请阅读文档!
现在让我们开始吧!
文件夹结构
为了保持代码库的整洁,文件夹结构至关重要,因此,我们先从按功能划分应用程序开始。如果您使用过,就会发现创建一个 bloc 文件夹,其中包含相应的状态、事件,以及一个用于功能 UI 部分的viewsflutter_bloc
文件夹,这种做法非常常见,如下所示:
VS Code bloc 扩展的地毯生成 v5.6.0
如你所见,我们有一个名为“笑话”的功能,你可以在这里找到笑话应用的逻辑和视图。许多使用过的人可能对这种结构很熟悉。flutter_bloc
但在查看代码之前,让我们先看看我们想要使用Riverpod、StateNotifier和Freezed实现的结构:
您可以看到,它与 flutter_bloc 提出的结构非常相似。但在本例中,由于 riverpod 的工作方式,我们拥有以下文件:
- jokes_state.dart
- 这个文件定义了StateNotifier可能发出的状态。常见的有:initial、loading、data和error。
- jokes_state.freezed.dart
- 这是一个包含我们在中定义的类的所有信息的文件
jokes_state.dart
,我们不必太担心它的内容,因为它是生成的代码。
- 这是一个包含我们在中定义的类的所有信息的文件
- jokes_state_notifier.dart
- 此文件包含 的定义
JokesNotifier
,它是 StateNotifier 的一个实现。这将是我们状态管理的核心,因为我们在这个类中定义了一些方法,用于在必要时更改和发出新的状态。
- 此文件包含 的定义
- jokes_provider.dart
- 这个文件定义了我们将在此功能中使用的不同类型的提供程序
Provider
。在本例中,我们需要两个,第一个是用于获取新笑话的存储库的通用提供程序,第二个是用于StateNotifiersStateNotifierProvider
对象的特殊提供程序。换句话说,我们可以说这个文件是此功能的依赖注入完成的地方。
- 这个文件定义了我们将在此功能中使用的不同类型的提供程序
但是对于最后一点,如果我们有两个使用相同提供商的不同功能会发生什么?
很简单,我们可以创建一个文件,在其中定义一些全局的提供程序,以便多个功能可以同时使用。像这样:
如果您已经阅读过文档,您就会知道这一点,但以防万一,我提醒您,将提供商与 riverpod 结合起来非常简单。
例如,让我们想象一个商店的场景,我们Provider
为支付方式(PaymentMethods
)定义一个,为 定义另一个提供商Checkout
,在这种情况下,Checkout
提供商需要PaymentMethods
提供商能够工作,并且由于ProviderReference
在使用 riverpod 创建新提供商时可用的参数,创建关系非常简单:
///PaymentMethods Provider
final _paymentMethodsProvider = Provider<PaymentMethods>(
(ProviderReference _) => PaymentMethods(),
);
///CheckOut Provider
final checkoutProvider = Provider<Checkout>(
(ProviderReference ref) => Checkout(
paymentMethodsProvider: ref.watch(_paymentMethodsProvider),
),
);
📖 如果您仍然不太理解最后一部分,我建议您阅读我在开始时留下的工具文章,这里。
让我们看看代码🔥
项目设置
你可能会嘲笑这一步,但你会惊讶于它被遗忘的次数之多。要使用 riverpod,你必须添加一个ProviderScope
小部件作为我们应用程序的根,这允许提供商进行通信,并且是使用 riverpod 的必需条件。
///main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_jokes_basic/src/app.dart';
void main() {
runApp(
ProviderScope(
child: RiverpodJokesApp(),
),
);
}
就我个人而言,我喜欢将main.dart与应用程序的其余部分分开,并创建一个包含应用程序根目录和所需的任何额外配置的app.dart文件。
///app.dart
import 'package:flutter/material.dart';
import 'package:riverpod_jokes_basic/src/features/jokes/views/jokes_page.dart';
class RiverpodJokesApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: JokesPage(),
);
}
}
就这样,我们就可以开始使用 riverpod 了。记得在pubspec.yaml文件中添加以下依赖项:
# pubspec.yaml
name: riverpod_jokes_basic
description: A new Flutter project.
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: ">=2.12.0 <3.0.0"
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
dio: ^4.0.0
equatable: ^2.0.0
flutter_riverpod: ^0.14.0+1
freezed_annotation: ^0.14.1
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ^1.12.2
freezed: ^0.14.1+2
json_serializable: ^4.1.0
flutter:
uses-material-design: true
模型
在编写应用程序的逻辑代码之前,我们需要创建代表 API 对象的模型。在本例中,我们将参考 Jokes API 提供的文档,其中提到响应格式如下:
/// JOKES API Response
{
"error": false,
"category": "Programming",
"type": "twopart",
"setup": "How many programmers does it take to screw in a light bulb?",
"delivery": "None. It's a hardware problem.",
"flags": {
"nsfw": false,
"religious": false,
"political": false,
"racist": false,
"sexist": false,
"explicit": false
},
"id": 1,
"safe": true,
"lang": "en"
}
只需查看响应格式,我们就能确定要使用的两个模型。第一个模型用于表示笑话的可能标志,我们称之为 ;FlagsModel
另一个模型用于表示整个笑话,我们称之为JokesModel
。
这些对象使用equatable和json_serializable来生成fromJson()
和toJson()
方法。
// flags_model.dart
import 'package:equatable/equatable.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'flags_model.g.dart';
@JsonSerializable()
class FlagsModel extends Equatable {
FlagsModel({
this.explicit,
this.nsfw,
this.political,
this.racist,
this.religious,
this.sexist,
});
//Json Serializable
factory FlagsModel.fromJson(Map<String, dynamic> json) =>
_$FlagsModelFromJson(json);
Map<String, dynamic> toJson() => _$FlagsModelToJson(this);
final bool? explicit;
final bool? nsfw;
final bool? political;
final bool? racist;
final bool? religious;
final bool? sexist;
@override
List<Object?> get props => [
explicit,
nsfw,
political,
racist,
religious,
sexist,
];
}
// jokes_model.dart
import 'package:equatable/equatable.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import './flags_model.dart';
part 'joke_model.g.dart';
@JsonSerializable()
class JokeModel extends Equatable {
JokeModel({
required this.safe,
this.category,
this.delivery,
this.flags,
this.id,
this.lang,
this.setup,
this.type,
});
//Json Serializable
factory JokeModel.fromJson(Map<String, dynamic> json) =>
_$JokeModelFromJson(json);
Map<String, dynamic> toJson() => _$JokeModelToJson(this);
final String? category;
final String? delivery;
final FlagsModel? flags;
final int? id;
final String? lang;
final bool safe;
final String? setup;
final String? type;
@override
List<Object?> get props => [
category,
delivery,
flags,
id,
lang,
safe,
setup,
type,
];
}
一旦创建了这两个类,就需要运行build_runner
命令,因为json_serializable必须生成类的解析方法。
我们运行以下命令:
# When the project depends on Flutter
flutter pub run build_runner build
# In project is pure dart
pub run build_runner build
这样,所有语法错误都会消失,我们可以进入下一步。
存储库
这次我不会过多关注这一步或任何清洁架构层/组件,因为这不是主要目标。如果您想查看更多关于清洁架构及其组件的示例或文档,可以查看我的网络,那里有其他讨论这些概念的文章、代码库和视频。
创建带有接口的存储库的原因当然是良好的编码实践,但也是为了让您看到在创建提供程序时如何使用接口进行依赖注入。
jokes_repository.dart包含接口及其实现。它们只有一个方法,用于调用 API 获取新的笑话。如果响应成功,则返回笑话,否则抛出异常。IJokesRepository
JokesRepository
Future<JokeModel> getJoke();
/// jokes_repository.dart
import 'package:dio/dio.dart';
import 'package:riverpod_jokes_basic/src/features/jokes/data/models/joke_model.dart';
abstract class IJokesRepository {
Future<JokeModel> getJoke();
}
class JokesRepository implements IJokesRepository {
final _dioClient = Dio();
final url = 'https://v2.jokeapi.dev/joke/Programming?type=twopart';
@override
Future<JokeModel> getJoke() async {
try {
final result = await _dioClient.get(url);
if (result.statusCode == 200) {
return JokeModel.fromJson(result.data);
} else {
throw Exception();
}
} catch (_) {
throw Exception();
}
}
}
使用 Freezed 的州
在本例中,我们生成随机数的功能并不复杂,目标是模拟 API 调用,并对 UI 中的不同状态做出反应。因此,我们将设置4 个状态:初始状态、加载状态、数据状态(当我们已经有了新的笑话时)以及错误状态(用于向用户反馈正在发生的事情)。
/// jokes_state.dart
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_jokes_basic/src/features/jokes/data/models/joke_model.dart';
part 'jokes_state.freezed.dart';
///Extension Method for easy comparison
extension JokesGetters on JokesState {
bool get isLoading => this is _JokesStateLoading;
}
@freezed
abstract class JokesState with _$JokesState {
///Initial
const factory JokesState.initial() = _JokesStateInitial;
///Loading
const factory JokesState.loading() = _JokesStateLoading;
///Data
const factory JokesState.data({required JokeModel joke}) = _JokesStateData;
///Error
const factory JokesState.error([String? error]) = _JokesStateError;
}
此外,我们添加了一个扩展方法来简化比较,以了解当前状态是否正在加载。
一旦我们用各自的Freezed 注释定义了状态的结构,我们就必须运行一个命令来创建带有生成代码的jokes_state.freezed.dart文件。
在终端中:
# In this case our project depend on flutter
# so we run
flutter pub run build_runner build
# If the project was pure dart we run
pub run build_runner build
这样,将生成新文件来解决状态文件中的语法错误。
⚠️ jokes_state.freezed.dart代码已生成,因此如果您不理解它,请不要感到压力或害怕,它不应该被修改。
实现 StateNotifier
我前面提到过,这是我们国家管理的核心。
在这个类中,我们将调用存储库,并分配状态,以便将它们通知给 UI 中的组件。
/// jokes_state_notifier.dart
part of 'jokes_provider.dart';
class JokesNotifier extends StateNotifier<JokesState> {
JokesNotifier({
required IJokesRepository jokesRepository,
}) : _jokesRepository = jokesRepository,
super(const JokesState.initial());
final IJokesRepository _jokesRepository;
Future<void> getJoke() async {
state = const JokesState.loading();
try {
final joke = await _jokesRepository.getJoke();
state = JokesState.data(joke: joke);
} catch (_) {
state = JokesState.error('Error!');
}
}
}
您可以看到,我们所做的第一件事是在super()
类的方法中将第一个状态分配给JokesState.initial()
。然后,我们只有一个getJoke()
方法在 中调用存储库,因为正如我们之前所见,在某些情况下我们会抛出异常。基于此,我们将在 API 调用成功时或抛出并被 捕获的异常时try {} catch {}
将新状态分配给。state = JokesState.data(joke: joke);
state = JokesState.error('Error!');
catch()
公开提供程序并注入依赖项
到目前为止,我们已经创建了应用程序逻辑所需的所有组件。现在我们必须创建两个提供程序。首先是用于注入依赖项(在本例中为 )JokesRepository
及其相应接口的提供程序;其次是用于暴露 StateNotifier 的提供程序(在本例中为StateNotifierProvider ),它暴露 ,JokesNotifier
以便我们能够对 UI 中的状态变化做出反应。
/// jokes_provider.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_jokes_basic/src/features/jokes/data/repositories/jokes_repository.dart';
import 'jokes_state.dart';
export 'jokes_state.dart';
part 'jokes_state_notifier.dart';
///Dependency Injection
//* Logic / StateNotifier
final jokesNotifierProvider = StateNotifierProvider<JokesNotifier, JokesState>(
(ref) => JokesNotifier(
jokesRepository: ref.watch(_jokesRepositoryProvider),
),
);
//* Repository
final _jokesRepositoryProvider = Provider<IJokesRepository>(
(ref) => JokesRepository(),
);
您可以看到,正如我之前在PaymentMethods/Checkout示例中向您展示的那样,组合提供程序非常简单,在这种情况下,我们使用来ProviderReference (ref)
注入JokesNotifier
存储库所具有的依赖关系。
有人可能会问,为什么我要将其声明_jokesRepositoryProvider
为私有变量?这只是为了避免犯从 UI 直接调用存储库的错误。
至此,我们已经完成了此应用程序所有必要逻辑组件的实现。现在我们只需要实现 UI 并响应状态。
对 UI 中的状态做出反应
为了简化示例,本例中 UI 只有一个页面。但在展示代码之前,我想强调两点。
显然要记得阅读文档,但只是提醒你:
如何调用 StateNotifierProvider 上的方法
如果只想调用一种方法,这种情况下我们需要调用StateNotifiergetJoke()
的方法:
/// To call a method
context.read(providerVariable.notifier).methodToCall();
/// In our case to call the state notifier
context.read(jokesNotifierProvider.notifier).getJoke();
如何倾听州政府的变化
如果您想监听StateNotifier发出的状态变化,则可以使用不同的方法,但最简单的方法是使用它ConsumerWidget
来访问名为 watch 类型的属性,ScopedReader
通过它我们可以监听所需提供者的不同状态变化。
/// Using a ConsumerWidget
class NewWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, ScopedReader watch) {
final state = watch(randomNumberNotifierProvider);
return ///Here you can do whatever you want with the state
}
}
在我们的例子中,它甚至更简单,因为当我们使用freezed创建状态时,我们可以访问该when ()
方法,这使我们能够更轻松地对每个状态做出反应,从而避免if - else
代码中许多条件的问题。
对于这个实现,我们可以有如下内容:
class _JokeConsumer extends ConsumerWidget {
@override
Widget build(BuildContext context, ScopedReader watch) {
final state = watch(jokesNotifierProvider);
return state.when(
initial: () => Text('Press the button to start'),
loading: () => Center(child: CircularProgressIndicator()),
data: (joke) => Text('${joke.setup}\n${joke.delivery}'),
error: (error) => Text('Error Occured!'),
);
}
}
这会ConsumerWidget
为每个不同的状态返回一个不同的小部件。
有了这个,我们就可以为我们的应用程序创建页面了:
/// jokes_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_jokes_basic/src/features/jokes/logic/jokes_provider.dart';
///JokesPage
class JokesPage extends StatelessWidget {
///JokesPage constructor
const JokesPage({Key? key}) : super(key: key);
///JokesPage [routeName]
static const routeName = 'JokesPage';
///Router for JokesPage
static Route route() {
return MaterialPageRoute<void>(builder: (_) => const JokesPage());
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Riverpod Jokes'),
),
body: Padding(
padding: EdgeInsets.symmetric(horizontal: 20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_JokeConsumer(),
const SizedBox(height: 50),
_ButtonConsumer(),
],
),
),
);
}
}
class _JokeConsumer extends ConsumerWidget {
@override
Widget build(BuildContext context, ScopedReader watch) {
final state = watch(jokesNotifierProvider);
return state.when(
initial: () => Text(
'Press the button to start',
textAlign: TextAlign.center,
),
loading: () => Center(
child: CircularProgressIndicator(),
),
data: (joke) => Text('${joke.setup}\n${joke.delivery}'),
error: (error) => Text('Error Occured!'),
);
}
}
class _ButtonConsumer extends ConsumerWidget {
@override
Widget build(BuildContext context, ScopedReader watch) {
final state = watch(jokesNotifierProvider);
return ElevatedButton(
child: Text('Press me to get a joke'),
onPressed: !state.isLoading
? () {
context.read(jokesNotifierProvider.notifier).getJoke();
}
: null,
);
}
}
结果如下:
它不是世界上最漂亮的用户界面,但这不是目标,而是学习如何将 riverpod、freezed 和 statenotifier 一起用作状态管理器。
如果您想看一个具有完整干净架构设置的示例,这是一个相当基本的示例,我为您提供以下链接:
QR 生成器应用程序,包含测试 flutter_bloc 和 riverpod 实现 + 简洁的架构
将来我计划分享一篇关于如何进行测试和其他事情的文章。
一如既往……
感谢我的朋友Marcos Sevilla提供这个结尾😂,他也分享了精彩的内容,所以如果你想关注内容,这里是他的Twitter 。
您可以分享本文以帮助其他开发人员在使用 Flutter 编写应用程序时继续提高生产力。
本文在 Medium 上有西班牙语版本。点击这里。别客气。🇪🇸
此外,如果您喜欢这些内容,您可以在我的社交媒体上找到更多内容并与我保持联系:
- dev.to——您正在阅读本文的地方。
- GitHub——如果您喜欢这些示例,我的代码存储库在哪里。
- GitHub NoScope——您可以在其中找到 YouTube 和 Twitch 频道中使用的代码存储库。
- LinkedIn——我的专业交流平台。
- Medium——我在这里发表我的西班牙语文章。
- Twitter——我在这里表达我的简短想法并分享我的内容。
- Twitch——我在这里进行非正式的现场表演,并从中截取包含特定信息的片段。
- YouTube——我在这里发布我生活中的片段。