Flutter 入门与实战(五十): Provider实现不相关页面状态共享

1,584 阅读5分钟

这是我参与8月更文挑战的第13天,活动详情查看:8月更文挑战

前言

我们之前讲述了 Provider 实现状态管理的两种方式:

那就来了一个问题,两个不相关的页面可以实现状态共享吗?比如我们的动态添加和编辑页面,属于同一个层级,如果不考虑利用他们的父级组件完成状态共享,那么两个页面就是平行不相关的,但是他们很多方法和数据都是可以共用的。对于这种情况,Provider 提供了一种方式来实现状态共享,那就是两个页面共用同一个状态对象。

MyChangeNotifier variable;

ChangeNotifierProvider.value(
  value: variable,
  child: ...
)

使用 ChangeNofitierProvider.value 方法可以利用一个已有的对象提供状态管理,而且这个已有的对象可以供其他组件共用。

改造状态管理类

添加和编辑页面的很多方法和数据是可以共用的,例如:

  • handleTextFieldChanged:文本输入值改变时更新对应表单内容;
  • handleClear:点击清除按钮时清除对应表单的内容;
  • handleImagePicked:图片选择控件选定图片后更新图片;
  • _imageFile:选择的图片文件。
  • _formData:表单的配置是共用的,但是添加页面没有初始值,而编辑页面需要使用已有动态填充。

这里我们既然要保证添加和编辑页面共用一个状态对象,那就需要提供类的共享对象,自然这就需要使用到静态的单例对象。我们把上一篇的 DynamicAddModel 改为 DynamicShareModel,使用单例定义:

class DynamicShareModel with ChangeNotifier {
  DynamicShareModel._privateConstructor();

  static final DynamicShareModel sharedDynamicModel =
      DynamicShareModel._privateConstructor();
  
  //...
}

同时,对于添加和编辑页面,进入前在渲染界面时都应该清除表单数据和缓存的动态对象,否则会导致添加页面和编辑页面会使用旧数据渲染界面(比如编辑完之后,进入添加页面会渲染刚刚编辑的表单数据)。因此,需要提供一个清除缓存状态数据的方法:

clearState() {
  _currentDynamic = null;
  _imageFile = null;
  _formData.forEach((key, form) {
    form['value'] = '';
    (form['controller'] as TextEditingController)?.text = '';
  });
}

然后就是完善添加和编辑的差异方法:

  • getDynamic:使用动态 id 获取当前的动态对象,成功后通知编辑界面渲染表单——按已有的动态填充表单。
  • handleAdd:添加动态的网络提交和处理方法。
  • handleEdit:编辑动态的网络提交和处理方法。
  • _validateForm::表单校验方法,对于添加页面需要额外校验是否选择了图片。

源码比较长,就不贴了,已经上传至:状态管理代码

页面代码改造

这个时候添加页面进入后我们需要先清除状态缓存,因此需要由生命周期管理了,需要把添加页面变为 StatefulWidget,然后在 initState 中清除状态缓存。

@override
void initState() {
  super.initState();

  context.read<DynamicShareModel>().clearState();
}

同时,在 DynamicAddWrapper 这个包裹类上,需要使用 ChangeNotifierProvider.value 的方式来和编辑页面共享共一个DynamicShareModel状态管理单例对象。

class DynamicAddWrapper extends StatelessWidget {
  DynamicAddWrapper({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider.value(
      value: DynamicShareModel.sharedDynamicModel,
      child: _DynamicAdd(),
    );
  }
}

编辑页面也类似,只是需要在 initState 中先清除状态缓存,然后再请求动态详情:

@override
void initState() {
  super.initState();
  context.read<DynamicShareModel>().clearState();
  context.read<DynamicShareModel>().getDynamic(widget.dynamicId);
}

编辑页面和添加页面的表单几乎一样,只是提交按钮名称和提交成功后处理逻辑不同。

// 添加页面
 DynamicForm(
  watchState.formData,
  readState.handleTextFieldChanged,
  readState.handleClear,
  '提交',
  () {
    readState.handleAdd().then((success) {
      if (success) {
        context.read<DynamicModel>().add(readState.currentDynamic);
        Navigator.of(context).pop();
      }
    });
  },
  readState.handleImagePicked,
  imageFile: watchState.imageFile,
),
);

//编辑页面
DynamicForm(
  watchState.formData,
  readState.handleTextFieldChanged,
  readState.handleClear,
  '保存',
  () {
    readState.handleEdit().then((success) {
      context.read<DynamicModel>().update(watchState.currentDynamic);
      Navigator.of(context).pop();
    });
  },
  readState.handleImagePicked,
  imageFile: watchState.imageFile,
  imageUrl: watchState.currentDynamic.imageUrl,
);

就这样,我们就完成了两个页面和共享状态管理的实现。

运行结果

运行结果和我们的预期一致,实际调试的时候也可以试着不清除缓存会怎么样。实际开发过程中很容易不小心出现忘记清除缓存的情况,因此,从代码维护性角度而言,共享状态也未必是最佳的选择,具体情况具体分析。

屏幕录制2021-08-12 下午10.34.10.gif

总结

本篇介绍了使用 ProviderChangeNotifierProvider.value的方式来在不相关的页面间共享状态数据。步骤为:

  • 创建两个页面都能访问到的同一个对象,通常是使用单例或容器的方式。
  • 创建一个包裹Widget,使用ChangeNotifierProvider.value包裹页面,以便访问到共享的状态对象。
  • 如果页面之间共享的数据可能因为另一个页面的操作而改变就需要看是否需要清除状态缓存(有些可能是需要同步而不是清除)。
  • 页面组件引用状态完成界面渲染和交互。

本篇仅为示例,实际写起来的过程中感觉还是有些别扭,其实状态共享会更适合对同一个对象的不同操作的场景,例如从详情页点击编辑进入编辑页面这种情况。本篇将添加和编辑放一起做状态共享虽然提高了代码复用性,但是降低了可维护性和破坏了单一职责原则,因此实际开发过程中,可以根据实际情况来决定要不要共享状态。


我是岛上码农,微信公众号同名,这是Flutter 入门与实战的专栏文章。

👍🏻:觉得有收获请点个赞鼓励一下!

🌟:收藏文章,方便回看哦!

💬:评论交流,互相进步!