Navigation Menu

Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TickerProviderStateMixin dispose 报错 #461

Closed
bitterking opened this issue Sep 11, 2019 · 14 comments
Closed

TickerProviderStateMixin dispose 报错 #461

bitterking opened this issue Sep 11, 2019 · 14 comments
Labels
the key issue the key issue

Comments

@bitterking
Copy link

在Lifecycle.dispose 中将正在执行的animationController dispose掉,会报错
image
感觉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();
});
}`

@zjuwjf
Copy link
Contributor

zjuwjf commented Sep 11, 2019

能提供一个最小的demo么?

@bitterking
Copy link
Author

https://github.com/bitterking/redex_sample

@zjuwjf 麻烦大佬看一下,谢谢~~

@MMMzq
Copy link

MMMzq commented Sep 12, 2019

@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;
}

zjuwjf pushed a commit that referenced this issue Sep 12, 2019
zjuwjf added a commit that referenced this issue Sep 12, 2019
Fix TickerProviderMixin’s dispose bug #461
@zjuwjf
Copy link
Contributor

zjuwjf commented Sep 12, 2019

感谢,复现了这个问题。

它是由于 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分支。

@zjuwjf
Copy link
Contributor

zjuwjf commented Sep 17, 2019

@MMMzq @bitterking 你们的问题解决了么?

@MMMzq
Copy link

MMMzq commented Sep 19, 2019

@zjuwjf
那个问题并没有解决

我写了的个example来重现: example

重现步骤
1.进去直接下拉刷新一次 (如果不刷新第一次是正常的)
2.展开ExpansionTile,点击里面的item (发现动画不能正常启动)

初步推断原因:
生命周期函数触发"错误":initState->initState->dispose,

@qq329401134
Copy link

我也是遇到了同样的问题

@zjuwjf
Copy link
Contributor

zjuwjf commented Sep 23, 2019

2.展开ExpansionTile,点击里面的item (发现动画不能正常启动)

你说的问题,我并没有复现, 不断下拉刷新,执行一切符合预期。 initState 两次 是因为有两个item。

我的环境:flutter v1.9.1-hotfixes

@MMMzq
Copy link

MMMzq commented Sep 23, 2019

@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

git

zjuwjf pushed a commit that referenced this issue Sep 24, 2019
@zjuwjf
Copy link
Contributor

zjuwjf commented Sep 24, 2019

@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 added a commit that referenced this issue Sep 24, 2019
@zjuwjf zjuwjf added the the key issue the key issue label Sep 24, 2019
@MMMzq
Copy link

MMMzq commented Sep 25, 2019

@zjuwjf

非常感谢,问题解决了😀.

但是我还有一个问题就是:为什么现在还是initState->initState->dispose这个流程呢🤔?
这个流程很反直觉,正常来讲不应该是initState->dispose->initState这个过程的么?

@zjuwjf
Copy link
Contributor

zjuwjf commented Sep 25, 2019

这个是flutter内部的处理流程, 从我理解,比较符合预期,先创建再释放。

一个listview下有2个item

initState item0
initState item1
下拉刷新
initState item2
initState item3
dispose 1
dispose 0

@zjuwjf zjuwjf closed this as completed Sep 27, 2019
@MMMzq
Copy link

MMMzq commented Oct 8, 2019

@zjuwjf 请问一下什么时候可以把这个改动发版?

@zjuwjf
Copy link
Contributor

zjuwjf commented Oct 9, 2019

@zjuwjf 请问一下什么时候可以把这个改动发版?

@MMMzq 已发布 0.2.7

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
the key issue the key issue
Projects
None yet
Development

No branches or pull requests

4 participants