TabBar和ChangeNotifierProvider的烦人问题

时间:2020-04-22 11:32:28

标签: flutter provider flutter-change-notifier

想象一个这样的场景。

例如:一大堆各种流派的电影。 这个想法是一个TabBar,每个Tab中都包含一个类型的列表电影。 在这种情况下,ChangeNotifier(例如MoviesBloc)非常适合需要,ChangeNotifierGenre无关。 在每个TabBarView子级中,在这里使用ChangeNotifierProvider.value是错误的,因为每个标签都需要保持其自己的MoviesBloc状态,因此我将为每种类型提供ChangeNotifierProvider的{​​{1}},然后MoviesBloc听。我将它们放在称为Consumer的包装器类中。

结果: -如果按顺序滑动每个标签,则没有错误。 -如果要刷很多标签。例如:突然从第一个标签到最后一个标签(例如有20个标签),尽管每个标签都是分别用自己的MoviesBlocView创建的,但控制台仍会抱怨重复使用已处置的ChangeNotifier

复制代码

ChangeNotifier

movies_bloc.dart

import 'package:flutter/material.dart'; class MoviesBloc extends ChangeNotifier { List<Movie> _result; BlocState _state = BlocState.idle; Future<void> getMoviesWithGenre(Genre genre) async { _setState(BlocState.loading); await Future.delayed(_delayTime); _result = List.generate( 20, (index) => Movie(id: index + 1, name: (index + 1).toString())); _setState(BlocState.loaded); } List<Movie> get result => _result; bool get loading => _state == BlocState.loading; BlocState get state => _state; void _setState(BlocState state) { _state = state; notifyListeners(); } } enum BlocState { idle, loading, loaded, } class Movie { Movie({ this.id, this.name, }); num id; String name; } class Genre { Genre({ this.id, this.name, }); num id; String name; } const _delayTime = Duration(seconds: 2);

movies_bloc_view.dart

class MoviesBlocView extends StatelessWidget { const MoviesBlocView({ Key key, @required this.bloc, @required this.loadedBuilder, }) : assert(bloc != null), assert(loadedBuilder != null), super(key: key); final MoviesBloc bloc; final Widget Function(BuildContext context, List<Movie> result) loadedBuilder; @override Widget build(BuildContext context) { return ChangeNotifierProvider<MoviesBloc>( create: (context) => bloc, child: Consumer<MoviesBloc>( builder: (context, bloc, child) { switch (bloc.state) { case BlocState.idle: return Container(); case BlocState.loading: return const Center(child: CircularProgressIndicator()); case BlocState.loaded: return loadedBuilder(context, bloc.result); default: return Container(); } }, ), ); } }

main.dart

更新

  1. 如果我们使用包装器import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'movies_bloc.dart'; import 'movies_bloc_view.dart'; List<Genre> genres = List.generate( 20, (index) => Genre(id: index + 1, name: (index + 1).toString())); class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: DefaultTabController( length: genres.length, child: NestedScrollView( headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { return <Widget>[ SliverAppBar( forceElevated: innerBoxIsScrolled, title: const Text('Provider'), bottom: TabBar( isScrollable: true, tabs: genres .map((genre) => Tab(child: Text(genre.name))) .toList(growable: false), ), ), ]; }, body: TabBarView( children: genres.map( (genre) { // use wrapper return MoviesBlocView( bloc: MoviesBloc()..getMoviesWithGenre(genre), loadedBuilder: (context, movies) => MoviesView( movies: movies, genre: genre, ), ); // or use provider directly return ChangeNotifierProvider<MoviesBloc>( create: (context) => MoviesBloc()..getMoviesWithGenre(genre), child: Consumer<MoviesBloc>( builder: (context, bloc, child) { switch (bloc.state) { case BlocState.idle: return Container(); case BlocState.loading: return const Center( child: CircularProgressIndicator()); case BlocState.loaded: return MoviesView( movies: bloc.result, genre: genre, ); default: return Container(); } }, ), ); }, ).toList(growable: false), ), ), ), ); } } class MoviesView extends StatefulWidget { const MoviesView({ Key key, @required this.movies, @required this.genre, }) : assert(movies != null), assert(genre != null), super(key: key); final List<Movie> movies; final Genre genre; @override _MoviesViewState createState() => _MoviesViewState(); } class _MoviesViewState extends State<MoviesView> with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; @override Widget build(BuildContext context) { super.build(context); // AutomaticKeepAliveClientMixin return ListView.builder( shrinkWrap: true, itemCount: widget.movies.length, itemBuilder: (_, index) { return ListTile( title: Text('Movie: ${widget.movies[index].name}'), trailing: Text('Genre: ${widget.genre.name}'), ); }, ); } } void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: HomePage(), ); } } ,则无论是否在创建MoviesBlocView时调用getMoviesWithGenre,控制台仍然会抱怨
MoviesBloc
  1. 如果我们不使用════════ Exception caught by widgets library ═══════════════════════════════════ The following assertion was thrown building Consumer<MoviesBloc>(dirty, dependencies: [_DefaultInheritedProviderScope<MoviesBloc>]): A MoviesBloc was used after being disposed. Once you have called dispose() on a MoviesBloc, it can no longer be used. The relevant error-causing widget was Consumer<MoviesBloc> lib\test_tabs\movies_bloc_view.dart:23 When the exception was thrown, this was the stack #0 ChangeNotifier._debugAssertNotDisposed.<anonymous closure> package:flutter/…/foundation/change_notifier.dart:105 #1 ChangeNotifier._debugAssertNotDisposed package:flutter/…/foundation/change_notifier.dart:111 #2 ChangeNotifier.addListener package:flutter/…/foundation/change_notifier.dart:141 #3 ListenableProvider._startListening package:provider/src/listenable_provider.dart:87 #4 _CreateInheritedProviderState.value package:provider/src/inherited_provider.dart:433 ...
      尝试在MoviesBlocView创建时调用getMoviesWithGenre的控制台会抱怨。很奇怪,现在它有一个不同的错误日志
MoviesBloc
  • 当我们在创建E/flutter (30300): [ERROR:flutter/lib/ui/ui_dart_state.cc(157)] Unhandled Exception: A MoviesBloc was used after being disposed. E/flutter (30300): Once you have called dispose() on a MoviesBloc, it can no longer be used. [38;5;244mE/flutter (30300): #0 ChangeNotifier._debugAssertNotDisposed.<anonymous closure>[39;49m [38;5;244mE/flutter (30300): #1 ChangeNotifier._debugAssertNotDisposed[39;49m [38;5;244mE/flutter (30300): #2 ChangeNotifier.notifyListeners[39;49m [38;5;248mE/flutter (30300): #3 MoviesBloc._setState[39;49m [38;5;248mE/flutter (30300): #4 MoviesBloc.getMoviesWithGenre[39;49m E/flutter (30300): <asynchronous suspension> [38;5;248mE/flutter (30300): #5 HomePage.build.<anonymous closure>.<anonymous closure>[39;49m [38;5;248mE/flutter (30300): #6 _CreateInheritedProviderState.value[39;49m [38;5;248mE/flutter (30300): #7 _InheritedProviderScopeMixin.value[39;49m [38;5;248mE/flutter (30300): #8 Provider.of[39;49m [38;5;248mE/flutter (30300): #9 Consumer.buildWithChild[39;49m [38;5;248mE/flutter (30300): #10 SingleChildStatelessWidget.build[39;49m [38;5;244mE/flutter (30300): #11 StatelessElement.build[39;49m [38;5;248mE/flutter (30300): #12 SingleChildStatelessElement.build[39;49m [38;5;244mE/flutter (30300): #13 ComponentElement.performRebuild[39;49m [38;5;244mE/flutter (30300): #14 Element.rebuild[39;49m [38;5;244mE/flutter (30300): #15 ComponentElement._firstBuild[39;49m [38;5;244mE/flutter (30300): #16 ComponentElement.mount[39;49m [38;5;248mE/flutter (30300): #17 SingleChildWidgetElementMixin.mount[39;49m [38;5;244mE/flutter (30300): #18 Element.inflateWidget[39;49m [38;5;244mE/flutter (30300): #19 Element.updateChild[39;49m [38;5;244mE/flutter (30300): #20 ComponentElement.performRebuild[39;49m [38;5;248mE/flutter (30300): #21 _InheritedProviderScopeMixin.performRebuild[39;49m [38;5;244mE/flutter (30300): #22 Element.rebuild[39;49m [38;5;244mE/flutter (30300): #23 ComponentElement._firstBuild[39;49m [38;5;244mE/flutter (30300): #24 ComponentElement.mount[39;49m [38;5;244mE/flutter (30300): #25 Element.inflateWidget[39;49m [38;5;244mE/flutter (30300): #26 Element.updateChild[39;49m [38;5;244mE/flutter (30300): #27 ComponentElement.performRebuild[39;49m [38;5;244mE/flutter (30300): #28 Element.rebuild[39;49m [38;5;244mE/flutter (30300): #29 ComponentElement._firstBuild[39;49m [38;5;244mE/flutter (30300): #30 ComponentElement.mount[39;49m [38;5;248mE/flutter (30300): #31 SingleChildWidgetElementMixin.mount[39;49m [38;5;244mE/flutter (30300): #32 Element.inflateWidget[39;49m [38;5;244mE/flutter (30300): #33 Element.updateChild[39;49m [38;5;244mE/flutter (30300): #34 ComponentElement.performRebuild[39;49m [38;5;244mE/flutter (30300): #35 Element.rebuild[39;49m [38;5;244mE/flutter (30300): #36 ComponentElement._firstBuild[39;49m [38;5;244mE/flutter (30300): #37 ComponentElement.mount[39;49m [38;5;244mE/flutter (30300): #38 Element.inflateWidget[39;49m [38;5;244mE/flutter (30300): #39 Element.updateChild[39;49m [38;5;244mE/flutter (30300): #40 SingleChildRenderObjectElement.mount[39;49m [38;5;244mE/flutter (30300): #41 Element.inflateWidget[39;49m [38;5;244mE/flutter (30300): #42 Element.updateChild[39;49m E/flutter (30300): #43 SingleChildRenderObjectElement.mount (package:flutter/sr E/flutter (30300): [ERROR:flutter/lib/ui/ui_dart_state.cc(157)] Unhandled Exception: A MoviesBloc was used after being disposed. E/flutter (30300): Once you have called dispose() on a MoviesBloc, it can no longer be used. [38;5;244mE/flutter (30300): #0 ChangeNotifier._debugAssertNotDisposed.<anonymous closure>[39;49m [38;5;244mE/flutter (30300): #1 ChangeNotifier._debugAssertNotDisposed[39;49m [38;5;244mE/flutter (30300): #2 ChangeNotifier.notifyListeners[39;49m [38;5;248mE/flutter (30300): #3 MoviesBloc._setState[39;49m [38;5;248mE/flutter (30300): #4 MoviesBloc.getMoviesWithGenre[39;49m E/flutter (30300): <asynchronous suspension> [38;5;248mE/flutter (30300): #5 HomePage.build.<anonymous closure>.<anonymous closure>[39;49m [38;5;248mE/flutter (30300): #6 _CreateInheritedProviderState.value[39;49m [38;5;248mE/flutter (30300): #7 _InheritedProviderScopeMixin.value[39;49m [38;5;248mE/flutter (30300): #8 Provider.of[39;49m [38;5;248mE/flutter (30300): #9 Consumer.buildWithChild[39;49m [38;5;248mE/flutter (30300): #10 SingleChildStatelessWidget.build[39;49m [38;5;244mE/flutter (30300): #11 StatelessElement.build[39;49m [38;5;248mE/flutter (30300): #12 SingleChildStatelessElement.build[39;49m [38;5;244mE/flutter (30300): #13 ComponentElement.performRebuild[39;49m [38;5;244mE/flutter (30300): #14 Element.rebuild[39;49m [38;5;244mE/flutter (30300): #15 ComponentElement._firstBuild[39;49m [38;5;244mE/flutter (30300): #16 ComponentElement.mount[39;49m [38;5;248mE/flutter (30300): #17 SingleChildWidgetElementMixin.mount[39;49m [38;5;244mE/flutter (30300): #18 Element.inflateWidget[39;49m [38;5;244mE/flutter (30300): #19 Element.updateChild[39;49m [38;5;244mE/flutter (30300): #20 ComponentElement.performRebuild[39;49m [38;5;248mE/flutter (30300): #21 _InheritedProviderScopeMixin.performRebuild[39;49m [38;5;244mE/flutter (30300): #22 Element.rebuild[39;49m [38;5;244mE/flutter (30300): #23 ComponentElement._firstBuild[39;49m [38;5;244mE/flutter (30300): #24 ComponentElement.mount[39;49m [38;5;244mE/flutter (30300): #25 Element.inflateWidget[39;49m [38;5;244mE/flutter (30300): #26 Element.updateChild[39;49m [38;5;244mE/flutter (30300): #27 ComponentElement.performRebuild[39;49m [38;5;244mE/flutter (30300): #28 Element.rebuild[39;49m [38;5;244mE/flutter (30300): #29 ComponentElement._firstBuild[39;49m [38;5;244mE/flutter (30300): #30 ComponentElement.mount[39;49m [38;5;248mE/flutter (30300): #31 SingleChildWidgetElementMixin.mount[39;49m [38;5;244mE/flutter (30300): #32 Element.inflateWidget[39;49m [38;5;244mE/flutter (30300): #33 Element.updateChild[39;49m [38;5;244mE/flutter (30300): #34 ComponentElement.performRebuild[39;49m [38;5;244mE/flutter (30300): #35 Element.rebuild[39;49m [38;5;244mE/flutter (30300): #36 ComponentElement._firstBuild[39;49m [38;5;244mE/flutter (30300): #37 ComponentElement.mount[39;49m [38;5;244mE/flutter (30300): #38 Element.inflateWidget[39;49m [38;5;244mE/flutter (30300): #39 Element.updateChild[39;49m [38;5;244mE/flutter (30300): #40 SingleChildRenderObjectElement.mount[39;49m [38;5;244mE/flutter (30300): #41 Element.inflateWidget[39;49m [38;5;244mE/flutter (30300): #42 Element.updateChild[39;49m E/flutter (30300): #43 SingleChildRenderObjectElement.mount (package:flutter/sr 时不调用getMoviesWithGenre时没有错误,但是这没有任何意义,因为如果我们不那样做级联,那么我不会不知道该怎么做,例如在创建时获取请求

感谢您的帮助!

更新

经过一些调试后,我发现这是因为TabBar的行为异常。它会尝试同时预加载最近的选定标签或其他内容,然后在同一标签中连续两次而不是一次丢弃它。

正如Remi所指出的,我需要验证我的MoviesBloc在处置时是否未调用notifyListeners(),将验证添加到我的MoviesBloc后可以正常工作

MoviesBloc

但这不是我所期望的 bool _mounted = true; bool get mounted => _mounted; @override void dispose() { _mounted = false; super.dispose(); } void _setState(BlocState state) { if (!mounted) return; _state = state; notifyListeners(); } 的行为,因为我在创建时就调用了该函数,所以我永远也无法想象它在被处置后会调用该函数。

MoviesBloc

在这一点上,我真的不知道TabBar和TabBarView到底发生了什么。如果有人能解释为什么TabBar这样做,我将不胜感激!

2 个答案:

答案 0 :(得分:2)

您的提供商设置错误:create: (context) => bloc。请改用ChangeNotifierProvider<MoviesBloc>.value(value: bloc)

答案 1 :(得分:0)

重写@override dispose 方法 MoviesBloc 但不要调用 super.dispose()

// Do rewrite dispose like this
@override
void dispose(){
  // dummy dispose and dummy statement
}
// Don't rewrite dispose like this
@override
void dispose();