使用Provider解决Riverpod的参数依赖

上一篇文章详细说明了状态管理在开发中的位置和所依赖的基础方法,帧与帧之间的变化是对应状态变化的体现,但每个框架都有其侧重点,Getx侧重简单,简单的页面,简单的状态管理,相对应的是复杂参数, 以及依赖传递时非常臃肿,需要使用很多Listen来同步不同的状态。Bloc有完整的filter状态转移的监听,非常适合编辑器等复杂状态,例如回退操作,这两者一个适合简单业务,一个适合复杂协同业务,大多数项目都没有那么极端,所以Flutter官方推荐了Riverpod作为首推的状态管理工具。

具体对比请移步 一篇文章,告别Flutter状态管理争论,问题和解决

任何工具都有缺点,与此同时,就会有一个或者丑陋,或者优雅的Work around级别的解决方案,riverpod也是,这篇文章试图解决riverpod丑陋的参数传递问题。例如: 如下场景,一个日程任务有多种来源和多个视图才能确定某条任务, 我们假定参数为来源日期来源计划来源看板,那我们定位这一条任务就需要如下代码

dart 复制代码
/// 声明状态,需要三个入参
@riverpod
class TaskDetail extends _$TaskDetail {
  @override
  TaskModel build(int taskId, int planId, int viewId) {
    return service.query(taskId, planId, viewId);
  }
}
/// 使用需要明确的三个参数
class TaskDetailScreen extends HookConsumerWidget {
  final int taskId;
  final int planId;
  final int viewId;

  const TaskDetailScreen({
    super.key,
    required this.taskId,
    required this.planId,
    required this.viewId,
  });

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final taskModel = ref.watch(taskDetailProvider(taskId, planId, viewId));
    return Container();
  }
}

目前为止这段代码没有体现出任何的缺点,反而做到了状态的声明使用的分离, 对于简单到中等复杂页面,这非常友好,如果我们选择使用非组件化,将所有代码写到这个TaskDetailScreen那将没有任何问题。因为不涉及参数传递或者指针(notifier)传递。但实际复杂的项目中,通常可重用可阅读也是很重要的指标,这个时候我们不得不考虑使用组件来提升这两个属性。例如:我们有一个富文本组件, 如果我们只有简单的交互,我们可以通过传递taskModel或者增加Callback(String)等方法,与provider进行交互,但这通常会写成如下代码。

dart 复制代码
/// 需要透传参数或者notifer, 或者抛出Callback
class RichEditor extends ConsumerWidget {
  final int taskId;
  final int planId;
  final int viewId;

  const RichEditor({
    super.key,
    required this.taskId,
    required this.planId,
    required this.viewId,
  });

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return GestureDetector(
      onTap: () => ref.read(taskDetailProvider(taskId, planId, viewId).notifier).saveContent('content'),
      child: Text('editor'),
    );
  }
}

这是非常丑陋的,虽然这种情况在使用Bloc时会被非常优雅的解决,但也可以通过一些结合来尝试解决中等复杂难度的场景。

分析痛点和难点

riverpod在这个场景下的痛点时需要透传参数,且组件化时非常丑陋,容易出错,那他的难点在于什么?在于全局的Scope, 所有全局的管理,例如Getx等都会面临多个层级重复页面,多参数的在同一个路由栈这样常见的问题,或者同时显示多个相同组件(同一个provider)。难点在于无法隔离,也就是跟Context关联,局部状态。

找到难点,我们就可以从这个点出发去尝试解决这个问题,或者是这种特定用例下的问题,在不脱离riverpod的情况下,我们的直观选择是在局部使用ProviderScope, 例如:

dart 复制代码
 ProviderScope(overrides: [
      sharedPrefsProvider.overrideWithValue(sharedPrefs),
    ], child: const TaskDetailScreen()),

这是一种方案,但这种不符合riverpod的最佳实践,也容易造成状态管理的混乱,第二种方案是将参数使用其它方式进行Context相关的关联,比如通过自定义的InheritedWidget 这种方案类似于ThemeData的局部化处理,这种方案理解简单,但需要不同的页面和不同的Provider定制化, 第二种方案明显也丑陋的,虽然解决了部分问题, 但需要引入自定义的Scope。

组合Hook?

我们知道Riverpod是状态分离的,也就是声明和管理使用状态是完全分离的,所以一个简单好用的界面内状态就可以极大简化这种类型的状态处理。所以,Flutter Hook就完美的补充了这部分。例如

dart 复制代码
final calendarFormat = useState(CalendarFormat.week);

TableCalendar(
          calendarFormat: calendarFormat.value,
          onFormatChanged: (format) {
            if (format != calendarFormat.value) {
              calendarFormat.value = format;
            }
          },
        ),

这里状态只和页面有关,声明使用修改 都在一个build函数中完成,所以使用注解riverpod是有一点冗余的。本以为可以像React一样,子组件可以跨级获取父组件的状态,后来发现flutter_hook并不是如此,hook和riverpod结合时,hook更像是一个完全的局部管理,并没有Recact Scope这种概念,跨级传递状态。

组合Bloc ?

Bloc是重量级的,声明需要BlocProvider, 消费需要使用BlocConsumer<BlocA, BlocAState>, 这也有点丑陋,虽然我们避免了每一个需要复杂参数都需要声明一个单独的InheritedWidget, 但同样多了很多模版代码。如果是这样的组合,加重了页面的复杂性以及阅读理解难度, 不如直接使用Bloc。

反思, 是不是违背了设计的初衷?

当笔者处处碰壁的时候,想起了之前为了解决打点参数透传而写的一个库data_trakcer, 似乎两者是同样的问题,但不同于打点的简单字段,这里需要明确的状态通知程序。 回顾响应式的设计原则 数据向下传递,操作向上传递 , 似乎认为难点或者痛点其实本不应该被关注 , 因为按照设计原则,一切都是合理的,我们必须对每个有操作的组件回调到声明notifer进行调用,或者将参数notifer进行一级一级的传递。

应该如何是好?

上述思考的过程,让笔者重新梳理了主流的一些状态管理,但还有很多是我不曾使用和了解的,笔者也不确定是否有其他方案解决了我所头疼的问题,也或许根本不是问题。当我在看Bloc Flutter时,笔者发现,Bloc实现局部Scope的原理其实底层是Provider。这个被遗忘的基础状态管理

dart 复制代码
static T of<T extends StateStreamableSource<Object?>>(
    BuildContext context, {
    bool listen = false,
  }) {
    try {
      return Provider.of<T>(context, listen: listen);
    } on ProviderNotFoundException catch (e) {
    }
  }

所以,是否可以使用Provider + Rivderpod解决我所遇到的问题?如下代码:

dart 复制代码
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:https_sync_client/domain/task_provider.dart';
import 'package:provider/provider.dart' as provider;

class TaskDetailScreen extends HookConsumerWidget {
  final int taskId;
  final int planId;
  final int viewId;

  const TaskDetailScreen({
    super.key,
    required this.taskId,
    required this.planId,
    required this.viewId,
  });

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final taskDetailNotifier =
        ref.watch(taskDetailProvider(taskId, planId, viewId).notifier);
    return provider.Provider(
      create: (context) => taskDetailNotifier,
      child: const RichEditor(),
    );
  }
}

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return GestureDetector(
      onTap: () {
        context.read<TaskDetail>().update(content: "");
      },
      child: Center(
        child: Container(
          color: Colors.amber,
          width: 100,
          height: 100,
        ),
      ),
    );
  }
}

这段代码似乎成功了,又似乎解决了我的痛点。成本似乎只有provider.Provider一个容器和context.read。能不能真的实现痛点,希望各位自己验证一下,实践才是检验真理的唯一标准 , 笔者也不确定这么操作是不是符合最佳实践,但在笔者工作当中两个场景是非常令人头痛的,一个打点重运营类 项目,经常需要按钮级别的打点需求, 第二是,业务复杂的组件且会堆叠的情况。

总结

遇到问题,解决问题是技术人的一个思考准则,当我们遇到丑陋 代码时,总可以找到合适的姿势去改变一点,使之优雅好用一点,世界上没有银弹, 但软件开发领域因为不断的进步,也在逐渐的越来越好(至少不想回去做Android原生)。可以把疑问放到评论区,一起讨论。

相关推荐
持久的棒棒君8 分钟前
ElementUI 2.x 输入框回车后在调用接口进行远程搜索功能
前端·javascript·elementui
2401_8572979119 分钟前
秋招内推2025-招联金融
java·前端·算法·金融·求职招聘
Python私教1 小时前
Flutter概述及其优势
flutter
undefined&&懒洋洋1 小时前
Web和UE5像素流送、通信教程
前端·ue5
winkee3 小时前
在 git commit 中使用 gpg key 进行签名
架构·前端框架·代码规范
大前端爱好者3 小时前
React 19 新特性详解
前端
小程xy3 小时前
react 知识点汇总(非常全面)
前端·javascript·react.js
随云6323 小时前
WebGL编程指南之着色器语言GLSL ES(入门GLSL ES这篇就够了)
前端·webgl
随云6323 小时前
WebGL编程指南之进入三维世界
前端·webgl
Dylanioucn3 小时前
【分布式微服务云原生】掌握 Redis Cluster架构解析、动态扩展原理以及哈希槽分片算法
算法·云原生·架构