概念
“状态管理”是指在应用程序中有效地组织、更新和共享数据的方式。
比起数据库和本地之类的持久层,有时我们需要存储一些应用运行过程中的临时数据,其中大部分可能都不会存入数据库。因此,
状态
这个词还是挺贴切的。
比较熟悉的是vue中Pinia提供的状态管理,他是全局可访问的,而且是响应式的。
数据传递
比较简单的父子组件传递数据。
父-->子
父组件通过属性、构造等,直接将数据传递给子组件。子-->父
父组件提供回调函数,使子组件调用该函数,并传入值,使得父组件获取到值并做出反应。
然而显然这种简单的做法并不普适,多级时,比如父亲和孙子,父亲需要将值先传递给儿子,再由儿子转交给孙子,然而儿子并不关心这个数据。从结果上来说,你在儿子那里多写了不必要的代码,然而使用这种方法时不得不写。
全局对象
那么全局的方案就值得思考了。我们使用全局对象,将两个本不相关的组件联系了起来。
- 需要通信的组件都会依赖这个全局对象
- 不进行通信的组件将会不受影响。
组件UI的刷新
状态管理本身通常不直接包含 UI 界面的刷新概念,但它是驱动 UI 更新的关键部分。
因为很多情况下,我们希望对某个组件的操作,能够影响另一个组件。
Pinia
在Pinia和Vue中,存储的数据是响应式的,数据改变后,页面会自动刷新。
Fluuter的Provider
Flutter中,官方Provider库,Notifier这一系列解决方法,类似于父子之间的通信。
- 在祖宗组件上绑定数据源。
- 在后代组件上访问数据。
- 在数据源的定义处,定义方法来更改数据并通知页面进行刷新。
对于对UI有影响的数据,我们使用以下方法对UI进行重构。
notifyListeners();
Provider的做法,是观察者模式,即订阅=>通知
。
然而存在比较大的缺陷,首先,你需要为两个进行通信的组件的最近的父节点套一层ChangeNotifierProvider
。
比如,如图片所示,红框框起来的两个组件进行通信(数据传递),我们需要在节点树往上找,找到共同的祖宗节点。
数据,也就是Notifier的实例,寄宿在祖宗节点上,下面的任意一个节点都可以访问到这些数据。
那么显然面临着一个问题。
- 没有共同祖宗节点的组件无法传递数据。
除此之外还面临着一个问题(因为我进行了相关测试),如果给两个节点的父节点绑定了相同的数据源,那么默认写法是两个不同的实例,所以数据肯定也不同,因此我尝试了单例模式。但是还是不行,在Flutter里面,页面和ViewModel是绑定的,如果页面销毁,ViewModel也会销毁,而销毁后我们依然去访问,就会出现问题。
- ViewModel会随着页面销毁。
这里的ViewModel,和前面的数据源是同一个东西。
下面是一些废话
水平太低,踩了很多坑。
我硬着头皮用了Provider,解决这个问题应该还是有一些思路。
再创建一个独立的ViewModel,而由于是从Hive数据库获取的数据,因此两个不同的ViewModel的数据可以做到同步。
问题在于,不同的ViewModel,其订阅者也是独立的,还记得我前面提到的观察者模式吗,ViewModel或者说Notifier更新数据后,需要手动调用notifyListeners();
方法来通知UI进行重构。由于两个页面各是各的ViewModel,订阅者也只是自己的页面中的组件,无法通知对方进行UI的刷新。
这个问题困扰了我一段时间,弄得很烦躁。
我刚才灵光一闪,想出了一个不太美观的方法。
- 假设组件A与B进行通信,各自创建ViewModel,绑到父组件上。
- ViewModel使用单例模式,由于单个页面对应单个不同的ViewModel,不会出现销毁了再次访问的问题。
- 定义一个方法调用notifyListener();作用仅仅是刷新界面。
void refresh(){notifyListeners();}
- 在页面需要刷新的时候,不仅调用自身ViewModel的方法,而且调用对方的refresh方法(上面定义的),这样就能完成页面刷新。
试了之后很快就失败了(虽然功能成功了),ViewModel不能设计为单例模式,因为页面销毁后就会同时销毁ViewModel,这时候去访问就会直接报错。因此前面的探究全都没有用,不仅做法丑陋,结果也很丑陋。