Closed bitterking closed 5 years ago
能提供一个最小的demo么?
https://github.com/bitterking/redex_sample
@zjuwjf 麻烦大佬看一下,谢谢~~
@bitterking 这个问题我好像遇到过
我之前的推断原因是:
flutter执行了"错误"的生命周期导致的:initState->initState->dispose,
导致dispose的是最新的controller。
暂时的解决办法: ctx.extra来保存需要销毁的AnimationController 像这样:
void _initState(Action action, Context<XxxxxState> ctx) {
....
ctx.extra["closeAnimation"] = clone.controller;
...
}
void _dispose(Action action, Context<XxxxState> ctx) {
(ctx.extra["closeAnimation"] as AnimationController)?.dispose();
ctx.extra["closeAnimation"] = null;
}
感谢,复现了这个问题。
它是由于 flutter 不是最合理的实现导致的
@override
void dispose() {
assert(() {
if (_tickers != null) {
for (Ticker ticker in _tickers) {
if (ticker.isActive) {
throw FlutterError('$this was disposed with an active Ticker.\n'
'$runtimeType created a Ticker via its TickerProviderStateMixin, but at the time '
'dispose() was called on the mixin, that Ticker was still active. All Tickers must '
'be disposed before or after calling super.dispose(). Tickers used by AnimationControllers '
'should be disposed by calling dispose() on the AnimationController itself. '
'Otherwise, the ticker will leak.\n'
'The offending ticker was: ${ticker.toString(debugIncludeStack: true)}');
}
}
}
return true;
}());
super.dispose();
}
mixin TickerProviderMixin<T> on Component<T> {
@override
_TickerProviderStfState<T> createState() => _TickerProviderStfState<T>();
}
class _TickerProviderStfState<T> extends ComponentState<T>
with TickerProviderStateMixin {}
当 Page mixin TickerProviderMixin 后
class WelcomePage extends Page<WelcomeState, Map<String, dynamic>>
with TickerProviderMixin<WelcomeState> {}
当执行到dispose 1、执行assert(!ticker.isActive) 2、执行ctx.onLifecycle 3、执行ctx.dispose()
所以还没进入我们的
void _dispose(Action action, Context<WelcomeState> ctx) {
ctx.state.controller.dispose();
}
前,就先assert报错了。
合理的应该Flutter 应该这个做
@override
void dispose() {
super.dispose();
assert(() {
if (_tickers != null) {
for (Ticker ticker in _tickers) {
if (ticker.isActive) {
throw FlutterError('$this was disposed with an active Ticker.\n'
'$runtimeType created a Ticker via its TickerProviderStateMixin, but at the time '
'dispose() was called on the mixin, that Ticker was still active. All Tickers must '
'be disposed before or after calling super.dispose(). Tickers used by AnimationControllers '
'should be disposed by calling dispose() on the AnimationController itself. '
'Otherwise, the ticker will leak.\n'
'The offending ticker was: ${ticker.toString(debugIncludeStack: true)}');
}
}
}
return true;
}());
}
目前fish-redux 内部处理了这个case。 下周一会在0.2.6中发布。 目前可以依赖master分支。
@MMMzq @bitterking 你们的问题解决了么?
@zjuwjf 那个问题并没有解决 我写了的个example来重现: example
重现步骤 1.进去直接下拉刷新一次 (如果不刷新第一次是正常的) 2.展开ExpansionTile,点击里面的item (发现动画不能正常启动)
初步推断原因: 生命周期函数触发"错误":initState->initState->dispose,
我也是遇到了同样的问题
2.展开ExpansionTile,点击里面的item (发现动画不能正常启动)
你说的问题,我并没有复现, 不断下拉刷新,执行一切符合预期。 initState 两次 是因为有两个item。
我的环境:flutter v1.9.1-hotfixes
@zjuwjf
我的版本信息:
Flutter 1.9.1+hotfix.2 • channel unknown • unknown source Framework • revision 2d2a1ffec9 (2 weeks ago) • 2019-09-06 18:39:49 -0700 Engine • revision b863200c37 Tools • Dart 2.5.0
@MMMzq 非常感谢你提供的这个case。
为什么通过 在effect-dispose中通过
ctx.state.controller.dispose();
会dispose最新的controller, 而非想要的dispose那份老的controller。
而通过
ctx.extra["controller''].dispose();
是正确的。
这引导我们去思考一个问题🤔: 状态的生命周期问题。
ctx.state 是一个函数,它总会根据固定的方法(connector)去获取自己最新的状态。在绝大多数场景(slots)下,它work的很好,但是在DynamicFlowAdapter场景下,情况会变的复杂。
一般而言,我们都说,组件是有生命周期的,状态是无生命周期的。
但是在@MMMzq 的case中,列表中的每一项状态,都是需要有生命周期的。
DynamicFlowAdapter的列表下 每一个组件如何获取它的最新state, 特别是 ListView 刷新下, 存在组件的复用和不复用的case下,组件如何获取它的state。
上面 @MMMzq 提到的问题就是,当为每一个组件设置一个唯一Key的时候,也就是每一个组件都不复用,那如何让刷新前的第1项的组件获得的state是刷新前的state,而让刷新后的第1项的组件获得的state是刷新后的state,尽管他们都是通过index来获取状态的。
再考虑这样的场景,当List下的某一个Item组件,发出一个action,被自己的reducer所处理,如何让列表下的组件合理的获取自己最新的state?
再考虑一个场景,当List 被插入一行数据的时候,状态和widget又是如何在被复用的?
下面解释下这个过程: 关键代码: 0.2.6
/// Define itemBean how to get state with connector
///
/// [_isSimilar] return true just use newState after reducer safely
/// [_isSimilar] return false we should use cache state before reducer invoke.
/// for reducer change state immediately but sub component will refresh on next
/// frame. in this time the sub component will use cache state.
Get<Object> _subGetter(Get<List<ItemBean>> getter, int index) {
final List<ItemBean> curState = getter();
final Object subCache = curState[index].data;
return () {
final List<ItemBean> newState = getter();
/// Either all sub-components use cache or not.
if (_isSimilar(curState, newState)) {
return newState[index].data;
} else {
return subCache;
}
};
}
/// Judge [oldList] and [newList] is similar
///
/// if true: means the list size and every itemBean type & data.runtimeType
/// is equal.
bool _isSimilar(
List<ItemBean> oldList,
List<ItemBean> newList,
) {
if (oldList != newList &&
oldList?.length == newList.length &&
Collections.isNotEmpty(newList)) {
bool isEvery = true;
for (int i = 0; i < newList.length; i++) {
if (oldList[i].type != newList[i].type ||
oldList[i].data.runtimeType != newList[i].data.runtimeType) {
isEvery = false;
break;
}
}
return isEvery;
}
return false;
}
最新的
/// Define itemBean how to get state with connector
///
/// [_isSimilar] return true just use newState after reducer safely
/// [_isSimilar] return false we should use cache state before reducer invoke.
/// for reducer change state immediately but sub component will refresh on next
/// frame. in this time the sub component will use cache state.
Get<Object> _subGetter(Get<List<ItemBean>> getter, int index) {
final List<ItemBean> curState = getter();
ItemBean cacheItem = curState[index];
return () {
final List<ItemBean> newState = getter();
/// Either all sub-components use cache or not.
if (newState != null && newState.length > index) {
final ItemBean newItem = newState[index];
if (_couldReuse(cacheItem, newItem)) {
cacheItem = newItem;
}
}
return cacheItem.data;
};
}
bool _couldReuse(ItemBean beanA, ItemBean beanB) {
if (beanA.type != beanB.type) {
return false;
}
final Object dataA = beanA.data;
final Object dataB = beanB.data;
if (dataA.runtimeType != dataB.runtimeType) {
return false;
}
final Object keyA = dataA is StateKey ? dataA.key() : null;
final Object keyB = dataB is StateKey ? dataB.key() : null;
return keyA == keyB;
}
修改后, Item状态的获取是否是最新的state,还是沿用上一份state, 完全取决于组件是否被复用,和ListView 下 Widget的复用机制吻合。
同时将Key的表达,推荐,迁移到State中来,让框架能感知到它。
class SuperItemState implements Cloneable<SuperItemState>, StateKey {
AnimationController controller;
Animation<Color> animationColor;
UniqueKey uniqueKey = UniqueKey();
SuperItemState();
@override
SuperItemState clone() {
return SuperItemState()
..controller = controller
..animationColor = animationColor
..uniqueKey = uniqueKey;
}
@override
Object key() {
return uniqueKey;
}
}
/// 同时不在需要在Component组合中提供 key
class SuperItemComponent extends Component<SuperItemState>
with
PrivateReducerMixin<SuperItemState>,
TickerProviderMixin<SuperItemState> {
SuperItemComponent()
: super(
effect: buildEffect(),
reducer: buildReducer(),
view: buildView,
// key: (state) => state.key(),
);
}
@zjuwjf
非常感谢,问题解决了😀.
但是我还有一个问题就是:为什么现在还是initState->initState->dispose
这个流程呢🤔?
这个流程很反直觉,正常来讲不应该是initState->dispose->initState
这个过程的么?
这个是flutter内部的处理流程, 从我理解,比较符合预期,先创建再释放。
一个listview下有2个item
initState item0 initState item1 下拉刷新 initState item2 initState item3 dispose 1 dispose 0
@zjuwjf 请问一下什么时候可以把这个改动发版?
@zjuwjf 请问一下什么时候可以把这个改动发版?
@MMMzq 已发布 0.2.7
在Lifecycle.dispose 中将正在执行的animationController dispose掉,会报错 感觉TickerProviderStateMixin 的dispose会先于ComponentState 中的dispose执行 `Effect buildEffect() {
return combineEffects(<Object, Effect>{
Lifecycle.initState: _init,
Lifecycle.dispose: _dispose,
SeatsAction.volumeChanged: _onVolumeChanged,
SeatsAction.selfVolumeChanged: _onSelfVolumeChanged,
});
}
void _init(Action action, Context ctx) {
final Object tickerProvider = ctx.stfState;
ctx.state.volumeAnimationControllers = List.generate(8, (index) {
return AnimationController(
duration: Duration(milliseconds: 1000), vsync: tickerProvider);
});
}
void _dispose(Action action, Context ctx) {
ctx.state.volumeAnimationControllers.forEach((controller) {
controller.dispose();
});
}`