如何在 Flutter 应用程序中开始使用 Riverpod、StateNotifier 和 Freezed。

2025-06-07

如何在 Flutter 应用程序中开始使用 Riverpod、StateNotifier 和 Freezed。

如何结合使用这 3 个工具来管理应用程序的状态。

Haz clic aquí para la 版本 en español

我们越来越多地听说RiverpodFlutter 中有一个全新的状态管理解决方案。最大的问题是,尝试使用它的人甚至不知道从哪里开始。

在本文中,我将重点介绍如何使用RiverpodStateNotiferFreezed一起构建项目以创建完整的状态管理流程。

我们要建造什么?

我们将构建一个笑话应用程序,它将使用Jokes API获取一个新的编程笑话,然后将其显示在屏幕上。结果将如下所示:

https://raw.githubusercontent.com/NoScopeDevs/riverpod_jokes_basic/main/assets/readme/app_result.gif

基础知识

我可以对每个工具都进行解释,但没必要重新发明轮子。我的朋友Marcos Sevilla有一些很棒的文章来解释这些工具。

  • Riverpod 文章
    • 该工具允许我们注入依赖项并封装状态,然后监听该状态。
  • StateNotifier 文章
    • 它是 ValueNotifier 的“重新实现”,不同之处在于它不依赖于 Flutter。
  • 冷冻物品
    • 它是不可变类的代码生成器。

现在您已经了解了这些工具各自是什么以及它们如何工作,让我们看看如何将它们一起使用作为状态管理解决方案。

⚠️ 在继续之前,了解工具的基本概念非常重要,因此请阅读文档!

现在让我们开始吧!

https://media.giphy.com/media/Y3MbPtRn74uR3Ziq4P/source.gif

文件夹结构

为了保持代码库的整洁,文件夹结构至关重要,因此,我们先从按功能划分应用程序开始。如果您使用过,就会发现创建一个 bloc 文件夹,其中包含相应的状态、事件,以及一个用于功能 UI 部分的viewsflutter_bloc文件夹,这种做法非常常见,如下所示:

https://raw.githubusercontent.com/NoScopeDevs/riverpod_jokes_basic/main/assets/readme/bloc_folder_structure.png

VS Code bloc 扩展的地毯生成 v5.6.0

如你所见,我们有一个名为“笑话”的功能,你可以在这里找到笑话应用的逻辑和视图。许多使用过的人可能对这种结构很熟悉flutter_bloc

但在查看代码之前,让我们先看看我们想要使用RiverpodStateNotifierFreezed实现的结构:

https://raw.githubusercontent.com/NoScopeDevs/riverpod_jokes_basic/main/assets/readme/riverpod_folder_structure.png

您可以看到,它与 flutter_bloc 提出的结构非常相似但在本例中,由于 riverpod 的工作方式,我们拥有以下文件:

  • jokes_state.dart
    • 这个文件定义了StateNotifier可能发出的状态。常见的有:initialloadingdataerror
  • jokes_state.freezed.dart
    • 这是一个包含我们在中定义的类的所有信息的文件jokes_state.dart,我们不必太担心它的内容,因为它是生成的代码。
  • jokes_state_notifier.dart
    • 此文件包含 的定义JokesNotifier,它是 StateNotifier 的一个实现。这将是我们状态管理的核心,因为我们在这个类中定义了一些方法,用于在必要时更改和发出新的状态。
  • jokes_provider.dart
    • 这个文件定义了我们将在此功能中使用的不同类型的提供程序Provider。在本例中,我们需要两个,第一个是用于获取新笑话的存储库的通用提供程序,第二个是用于StateNotifiersStateNotifierProvider对象的特殊提供程序。换句话说,我们可以说这个文件是此功能的依赖注入完成的地方。

但是对于最后一点,如果我们有两个使用相同提供商的不同功能会发生什么?

很简单,我们可以创建一个文件,在其中定义一些全局的提供程序,以便多个功能可以同时使用。像这样:

https://raw.githubusercontent.com/elian-ortega/Riverpod-StateNotifier-Freezed-Example/main/assets/shared_providers.png

如果您已经阅读过文档,您就会知道这一点,但以防万一,我提醒您,将提供商与 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),
  ),
);


Enter fullscreen mode Exit fullscreen mode

📖 如果您仍然不太理解最后一部分,我建议您阅读我在开始时留下的工具文章,这里。

让我们看看代码🔥

https://media.giphy.com/media/LmNwrBhejkK9EFP504/source.gif

项目设置

你可能会嘲笑这一步,但你会惊讶于它被遗忘的次数之多。要使用 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(),
    ),
  );
}


Enter fullscreen mode Exit fullscreen mode

就我个人而言,我喜欢将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(),
    );
  }
}


Enter fullscreen mode Exit fullscreen mode

就这样,我们就可以开始使用 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


Enter fullscreen mode Exit fullscreen mode

模型

在编写应用程序的逻辑代码之前,我们需要创建代表 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"
}


Enter fullscreen mode Exit fullscreen mode

只需查看响应格式,我们就能确定要使用的两个模型。第一个模型用于表示笑话的可能标志,我们称之为 ;FlagsModel另一个模型用于表示整个笑话,我们称之为JokesModel

这些对象使用equatablejson_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,
      ];
}


Enter fullscreen mode Exit fullscreen mode


// 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,
      ];
}


Enter fullscreen mode Exit fullscreen mode

一旦创建了这两个类,就需要运行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


Enter fullscreen mode Exit fullscreen mode

这样,所有语法错误都会消失,我们可以进入下一步。

存储库

这次我不会过多关注这一步或任何清洁架构层/组件,因为这不是主要目标。如果您想查看更多关于清洁架构及其组件的示例或文档,可以查看我的网络,那里有其他讨论这些概念的文章、代码库和视频。

创建带有接口的存储库的原因当然是良好的编码实践,但也是为了让您看到在创建提供程序时如何使用接口进行依赖注入。

jokes_repository.dart包含接口及其实现。它们只有一个方法,用于调用 API 获取新的笑话。如果响应成功则返回笑话,否则抛出异常。IJokesRepositoryJokesRepositoryFuture<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();
    }
  }
}


Enter fullscreen mode Exit fullscreen mode

使用 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;
}


Enter fullscreen mode Exit fullscreen mode

此外,我们添加了一个扩展方法来简化比较,以了解当前状态是否正在加载。

一旦我们用各自的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


Enter fullscreen mode Exit fullscreen mode

这样,将生成新文件来解决状态文件中的语法错误。

⚠️ jokes_state.freezed.dart代码已生成,因此如果您不理解它,请不要感到压力或害怕,它不应该被修改。

实现 StateNotifier

我前面提到过,这是我们国家管理的核心。

https://media.giphy.com/media/xUPGchIuMGVrjvVrDq/source.gif

在这个类中,我们将调用存储库,并分配状态,以便将它们通知给 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!');
    }
  }
}


Enter fullscreen mode Exit fullscreen mode

您可以看到,我们所做的第一件事是在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(),
);


Enter fullscreen mode Exit fullscreen mode

您可以看到,正如我之前在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();


Enter fullscreen mode Exit fullscreen mode

如何倾听州政府的变化

如果您想监听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
  }
}


Enter fullscreen mode Exit fullscreen mode

在我们的例子中,它甚至更简单,因为当我们使用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!'),
    );
  }
}


Enter fullscreen mode Exit fullscreen mode

这会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,
    );
  }
}


Enter fullscreen mode Exit fullscreen mode

结果如下:

https://raw.githubusercontent.com/elian-ortega/Riverpod-StateNotifier-Freezed-Example/main/assets/functionity.gif

它不是世界上最漂亮的用户界面,但这不是目标,而是学习如何将 riverpod、freezed 和 statenotifier 一起用作状态管理器。

如果您想看一个具有完整干净架构设置的示例,这是一个相当基本的示例,我为您提供以下链接:

使用 riverpod 创建的笑话应用程序遵循清洁架构原则

具有 COVID 19 API 和极简架构的应用程序

QR 生成器应用程序,包含测试 flutter_bloc 和 riverpod 实现 + 简洁的架构

将来我计划分享一篇关于如何进行测试和其他事情的文章。

一如既往……

感谢我的朋友Marcos Sevilla提供这个结尾😂,他也分享了精彩的内容,所以如果你想关注内容,这里是他的Twitter 。

您可以分享本文以帮助其他开发人员在使用 Flutter 编写应用程序时继续提高生产力。

本文在 Medium 上有西班牙语版本。点击这里。别客气。🇪🇸

此外,如果您喜欢这些内容,您可以在我的社交媒体上找到更多内容并与我保​​持联系:

  • dev.to——您正在阅读本文的地方。
  • GitHub——如果您喜欢这些示例,我的代码存储库在哪里。
  • GitHub NoScope——您可以在其中找到 YouTube 和 Twitch 频道中使用的代码存储库。
  • LinkedIn——我的专业交流平台。
  • Medium——我在这里发表我的西班牙语文章。
  • Twitter——我在这里表达我的简短想法并分享我的内容。
  • Twitch——我在这里进行非正式的现场表演,并从中截取包含特定信息的片段。
  • YouTube——我在这里发布我生活中的片段。
文章来源:https://dev.to/elianortega/how-to-start-using-riverpod-statenotifier-and-freezed-in-your-flutter-applications-181k
PREV
设置 Ubuntu 22.04 工作站用于软件开发和内容创建
NEXT
所以我完成了 Replit 的免费 100 天 Python 课程