【Flutter 工程】001-Flutter 状态管理:Riverpod

【Flutter 工程】001-Flutter 状态管理:Riverpod

一、概述

1、官方状态管理

状态管理处理应用程序数据流动和 UI 更新的关键概念。在 Flutter 应用程序中,状态管理确保应用程序 UI 和数据保持同步,共享和同步数据,并提供良好的代码结构和可维护性。

Flutter 提供了 StatefulWidget 作为最基本的状态管理方法。有状态组件可以存储和更新自身状态,适用于简单的场景和局部状态。

然而,StatefulWidget 存在以下问题:

  1. 状态管理复杂性:当组件树庞大且状态需要在多个组件之间共享时,状态管理变得复杂,代码难以理解和维护。
  2. 性能问题:相比 StatelessWidgetStatefulWidget 在状态变化时会导致更多组件重建,可能影响应用程序性能,尽管Flutter已经进行了性能优化。
  3. 生命周期管理复杂性StatefulWidget 具有复杂的生命周期,需要处理多个生命周期方法(如initStatedidUpdateWidget 和dispose),导致代码复杂和难以管理。
  4. 难以测试:由于 StatefulWidget 具有内部状态,编写单元测试和集成测试变得更加困难,可能影响应用程序的质量和可靠性。
  5. 重用性差StatefulWidget 的状态通常与特定实例紧密耦合,降低了组件的可重用性。

2、状态管理解决方案

在 Flutter 中,还有其他的状态管理方法可供选择,以下是一些常见的状态管理方法。

  1. InheritedWidgetInheritedModel:这些是 Flutter 提供的允许状态在组件树中向下传递的特殊类型的组件。它们可以帮助你在应用程序的不同层级之间共享状态。这种方法对于较小的应用程序或有限的状态共享需求较为合适
  2. Provider: 一个依赖注入和状态管理第三方库,它是在 InheritedWidget 基础上做了封装,有上面组件的能力,但是更简单易用。Provider 可以监听状态变化,并在需要时重新构建关联的组件。这种方法适用于各种规模的应用程序,具有良好的可扩展性和灵活性。
  3. Riverpod: 一个相对较新的状态管理库,类似于 Provider,但提供了更多的功能和改进。Riverpod 允许你创建不可变的、可组合的和可测试的状态管理解决方案。这种方法适用于需要更高度可控和可测试性的应用程序
  4. BLoC(Business Logic Component) :一种基于响应式编程的状态管理方法。BLoC 将业务逻辑与 UI 分离,使你可以轻松地测试和重用代码。BLoC 通常与 RxDart(一种 Dart 的响应式编程库)一起使用,以提供强大的数据流处理能力。这种方法适用于需要处理复杂业务逻辑和大量数据流的应用程序
  5. Redux: 一种集中式状态管理库,它将应用程序的状态存储在一个单一的状态树中。Redux 使用纯函数(称为reducers)来处理状态更新,使你可以轻松地跟踪和管理应用程序的状态变化。这种方法适用于需要严格的状态管理和可预测性的应用程序

具体选择什么样的状态管理方法,这取决于你应用程序的需求、复杂性和个人喜好。不同的方法有不同的优缺点,因此在选择状态管理方法时,请务必充分了解每种方法的特点,并权衡其适用性。

3、为什么选择 Riverpod

究其原因,还是 Riverpod 的一些主要特点比较给力,与我们的需求契合,且听我慢慢道来……

  1. 不可变性。Riverpod 中的状态是不可变的,这意味着状态在更新时会创建一个新的对象,而不是修改现有对象。这有助于减少错误,并使状态更易于理解和跟踪。
  2. 类型安全。Riverpod 在编译时提供了更强的类型安全性,有助于减少类型错误并提高代码质量。
  3. 无需 BuildContext。 与 Provider 不同,Riverpod 不依赖于 BuildContext 来访问状态。这使得在组件之外的位置(如函数或类)访问状态变得更加容易,同时提高了可测试性。
  4. 可组合。Riverpod 允许你组合不同的 Provider 以创建更复杂的状态管理解决方案。这有助于保持代码的模块化和可维护性。
  5. 易于测试。由于 Riverpod 的状态不依赖于 BuildContext,你可以更轻松地编写单元测试。此外,Riverpod 提供了用于模拟状态和测试的实用工具。
  6. 家族功能。Riverpod 具有所谓的“家族”功能,允许你根据参数创建多个相同类型的 Provider 实例。这使得在使用相同逻辑但参数不同的多个组件时,可以更好地管理状态。
  7. 非常灵活。Riverpod 具有很高的灵活性,可以很好地适应不同的应用程序结构和需求。你可以使用 Riverpod 来构建简单的局部状态管理,或者构建复杂的全局状态管理解决方案。

总之,Riverpod 是一个强大的状态管理库,适用于各种规模的 Flutter 应用程序。它提供了不可变性、类型安全性、无需 BuildContext 的访问、可组合性、易于测试和家族功能等多种优点。如果你正在寻找一个现代、灵活且易于使用的状态管理解决方案,Riverpod 是一个值得考虑的选择。

Riverpod 官方文档

https://docs-v2.riverpod.dev/zh-hans/

Riverpod 提供的几种 Provider

image-20230522142246669

二、官方示例

1、安装

flutter pub add flutter_riverpod dev:custom_lint dev:riverpod_lint riverpod_annotation dev:build_runner dev:riverpod_generator

2、官方示例

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

part 'main.g.dart';

// 我们创建一个 “provider”,它将用于保存一个值(这里是 “Hello world”)。
// 通过使用一个 provider,我们能够模拟或覆盖被暴露的值。

String helloWorld(HelloWorldRef ref) {
  return 'Hello world';
}

void main() {
  runApp(
    // 为了能让组件读取 provider,我们需要将整个
    // 应用都包裹在 “ProviderScope” 组件内。
    // 这里也就是存储我们所有 provider 状态的地方。
    ProviderScope(
      child: MyApp(),
    ),
  );
}

// 扩展来自 Riverpod 的 HookConsumerWidget 而不是 HookWidget
class MyApp extends ConsumerWidget {
  
  Widget build(BuildContext context, WidgetRef ref) {
    final String value = ref.watch(helloWorldProvider);

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Example')),
        body: Center(
          // 读取 provider 的值
          // 此处为了方便查看,设置了大字体
          child: Text(value, style: const TextStyle(fontSize: 40),),
        ),
      ),
    );
  }
}

3、代码生成

# --delete-conflicting-outputs 可选,会在生成代码冲突的时候,删除原来的代码,重新生成
flutter pub run build_runner build --delete-conflicting-outputs

4、官方示例运行结果

image-20230522133110041

三、基本使用

1、改造 main.dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:study/pages/HomePage.dart';

void main() {
  runApp(
    // 为了能让组件读取 provider,我们需要将整个
    // 应用都包裹在 “ProviderScope” 组件内。
    // 这里也就是存储我们所有 provider 状态的地方。
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {

    return const MaterialApp(
      home: HomePage(),
    );
  }
}

2、创建 home_page.dart

/lib/pages/home_page.dart

import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

import '../states/hello_state.dart';

class HomePage extends HookConsumerWidget {
  const HomePage({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    final hello = ref.watch(helloStateProvider);
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
      ),
      body: Center(
          child: SizedBox(
        height: 400,
        child: Column(
          children: [
            // 文本
            Text(hello.hello, style: const TextStyle(fontSize: 40),),
            // 更新文本
            ElevatedButton(
              style: ButtonStyle(minimumSize: MaterialStateProperty.all(const Size(200, 50))),
              onPressed: () {
                ref.read(helloStateProvider.notifier).setHello("文本更新了!");
              },
              child: const Text('Update'),
            ),
          ],
        ),
      )),
    );
  }
}

3、创建 hello_state.dart

lib/state/hello_state.dart

import 'package:flutter_riverpod/flutter_riverpod.dart';

class HelloState {
  final String hello;

  HelloState({
    this.hello = 'Hello World',
  });
}

class HelloStateProvider extends StateNotifier<HelloState> {
  HelloStateProvider() : super(HelloState());

  void setHello(String hello) {
    state = HelloState(
      hello: hello,
    );
  }
}

final helloStateProvider = StateNotifierProvider<HelloStateProvider, HelloState>(
  (ref) => HelloStateProvider(),
);

4、运行结果

image-20230522140032127

四、使用代码生成

代码生成是指使用工具为我们生成代码。

在Dart中,它的缺点是需要额外的步骤来“编译”应用。 尽管这个问题可能会在不久的将来得到解决, 但Dart团队正在研究并解决这个问题的潜在方案。

使用Riverpod时,代码生成是完全可选的。 当然你也完全可以不使用。

与此同时,Riverpod支持代码生成,且推荐你使用它。

1、改造 hello_state.dart

import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'hello_state.g.dart';


class HelloList extends _$HelloList {
  
  List<String> build() {
    return ["hello world!"];
  }

  void addHello(String hello) {
    state = [...state, hello];
  }
}

2、代码生成

# --delete-conflicting-outputs 可选,会在生成代码冲突的时候,删除原来的代码,重新生成
flutter pub run build_runner build --delete-conflicting-outputs

3、改造 home_page.dart

import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

import '../states/hello_state.dart';


class HomePage extends HookConsumerWidget {
  const HomePage({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    final List<String> hellos = ref.watch(helloListProvider);
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
      ),
      body: Center(
          child: SizedBox(
        height: 400,
        child: Column(
          children: [
            // 文本:遍历 hellos 列表
            ...hellos.map((e) => Text(e, style: const TextStyle(fontSize: 40),)),
            // 更新文本
            ElevatedButton(
              style: ButtonStyle(minimumSize: MaterialStateProperty.all(const Size(200, 50))),
              onPressed: () {
                // 获取当前时间
                DateTime now = DateTime.now();
                ref.read(helloListProvider.notifier).addHello("hello ${now.second}");
              },
              child: const Text('Update'),
            ),
          ],
        ),
      )),
    );
  }
}

4、运行结果

image-20230522141813454

5、为什么在Riverpod中使用代码生成

你可能在想:“如果在Riverpod中代码生成是可选的,为什么要使用?”

让你的代码生活更简单。

这包括但不限于:

  • 更好的语法, 更可读且更灵活,而且还能减少学习曲线。
    • 不需要担心FutureProviderProvider 还是其他 provider。仅需写下你的逻辑, Riverpod将为你选择最合适的provider。
    • 向provider传递参数现在不受限制。不再局限于使用 family 和传递单个参数, 现在可以传递任何形式的参数。这包括命名参数、可选参数甚至默认值。
  • 在Riverpod中编写的代码支持 有状态热重载
  • 更好地调试,通过生成额外的元数据然后用调试器调试。
  • Riverpod的一些功能将只支持代码生成。

与此同时,许多应用程序中已经使用了代码生成比如 Freezedjson_serializable。 在这种情况下,你的项目可能已经为代码生成配置好了,使用Riverpod应该很简单。

五、实践代码示例

1、状态

part 'film_state.g.dart';

part 'film_state.freezed.dart';


class FilmState with _$FilmState {
  factory FilmState({
    // EasyRefresh 控制器
    EasyRefreshController? controller,
    // 当前页码
    int? pageNum,
    // 每页数量
    int? pageSize,
    // 电影列表
    List<FilmModel>? list,
    // 没有更多数据了
    bool? noMore,
  }) = _FilmState;
}


class FilmStateNotifier extends _$FilmStateNotifier {
  
  FutureOr<FilmState> build() async {
    return FilmState(
      controller: EasyRefreshController(),
      pageNum: 0,
      pageSize: 10,
      list: [],
      noMore: false,
    );
  }

  // 加载电影列表
  FutureOr<void> loadFilmList() async {
    // 分页查询参数
    final dto = FilmPageRequestModel();
    dto.pageSize = state.value!.pageSize;
    dto.pageNum = state.value!.pageNum;
    // 加载电影列表
    BasePageModel basePageModel = await FilmApi.page(dto);
    basePageModel.content = basePageModel.decode(FilmModel(), basePageModel.content!);
    if (basePageModel.content!.isNotEmpty) {
      state.value!.list!.addAll(basePageModel.content!.cast<FilmModel>());
      state.value!.noMore = false;
    } else {
      state.value!.noMore = true;
    }
  }

  // 下拉刷新
  FutureOr<void> refresh() async {
    state.value!.list!.clear();
    state.value!.pageNum = 0;
    await loadFilmList();
    state.value!.controller!.finishRefresh(IndicatorResult.success, true);
  }

  // 上拉加载
  FutureOr<dynamic> loadMore() async {
    state.value!.pageNum = state.value!.pageNum! + 1;
    await loadFilmList();
    if (!state.value!.noMore!) {
      state.value!.controller!.finishLoad(IndicatorResult.success, true);
    } else {
      state.value!.controller!.finishLoad(IndicatorResult.noMore, true);
    }
  }
}

2、使用示例

class FilmPage extends ConsumerWidget {
  const FilmPage({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    final stateProvider = ref.watch(filmStateNotifierProvider);
    final stateNotifier = ref.watch(filmStateNotifierProvider.notifier);
    return stateProvider.when(
      data: (state) {
        return SimpleEasyRefresher(
          easyRefreshController: state.controller,
          onLoad: stateNotifier.loadMore,
          onRefresh: stateNotifier.refresh,
          childBuilder: (context, physics) {
            return ListView.builder(
              physics: physics,
              itemCount: state.list!.length,
              itemBuilder: (BuildContext context, int index) {
                return GestureDetector(
                  onTap: () => Navigator.push(context, MaterialPageRoute(builder: (context) => FilmDetailsPage(filmModel: state.list![index]))),
                  child: Container(color: GlobalColor.bgc, child: $BuildFilmItem(state.list![index])),
                );
              },
            );
          },
        );
      },
      loading: () => const CircularProgressIndicator(),
      error: (error, stackTrace) => Text('Error: $error'),
    );
  }
}

六、ref.readref.watch

1、ref.watch

  • ref.watch方法用于订阅状态,并在状态发生变化时重新构建小部件。它返回一个可监听的状态。
  • 当使用ref.watch订阅状态时,如果状态发生变化,相关的小部件会被重新构建,以更新界面展示。
  • ref.watch方法在小部件的build方法中使用,确保当状态变化时,与状态相关的部分会被更新。

2、ref.read

  • ref.read方法用于读取状态,但不会订阅状态变化。
  • 当使用ref.read读取状态时,它会立即返回当前的状态值,但不会自动更新界面
  • ref.read方法在小部件的build方法之外使用,例如在回调函数、事件处理程序或其他地方需要读取状态的情况下使用。

3、总结

  • 使用ref.watch订阅状态的变化并自动更新界面,适用于需要及时响应状态变化的情况。
  • 使用ref.read会立即返回当前的状态值,适用于不需要及时更新界面的情况。

七、XXXNotifierProviderXXXNotifierProvider.notifier的区别

1、XXXNotifierProvider

  • XXXNotifierProvider是一个 Provider 对象,它负责提供状态值。
  • 当使用ref.watch(XXXNotifierProvider)时,它会订阅状态的变化并返回状态值。

2、XXXNotifierProvider.notifier

  • notifierXXXNotifierProvider提供的一个特殊属性,它指向状态通知器(notifier)对象。
  • 状态通知器是一个用于管理和触发状态变更的对象,它实现了状态管理逻辑和业务逻辑
  • 当使用ref.watch(XXXNotifierProvider.notifier)时,它会订阅状态通知器的变化并返回状态通知器对象。

3、总结

  • XXXNotifierProvider返回的是Provider对象,用于访问状态值。
  • XXXNotifierProvider.notifier返回的是状态通知器对象,用于管理状态的变更和执行业务逻辑。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值