【问题标题】:An annoying problem with TabBar and ChangeNotifierProviderTabBar 和 ChangeNotifierProvider 的一个恼人问题
【发布时间】:2020-08-05 09:52:45
【问题描述】:

想象一下这样的场景。

例如:一大堆不同类型的电影。 这个想法是一个 TabBar,每个 Tab 都包含一个类型的电影列表。 在这种情况下,ChangeNotifier 例如MoviesBloc 将非常适合需要,ChangeNotifierGenre 无关。 在每个 TabBarView 孩子中,在这里使用 ChangeNotifierProvider.value 是错误的,因为每个选项卡都需要保持自己的 MoviesBloc 状态,所以我将为每种类型提供 MoviesBlocChangeNotifierProviderConsumer 来收听它.我将它们放在一个名为 MoviesBlocView 的包装类中。

结果: - 如果按顺序滑动每个选项卡,则没有错误。 - 如果刷大量的标签。例如:突然从第一个选项卡到最后一个选项卡(例如:有 20 个选项卡),控制台会抱怨重复使用已处理的 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

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(),
    );
  }
}

更新

  1. 如果我们使用包装器MoviesBlocView,无论是否在MoviesBloc创建时调用getMoviesWithGenre,控制台仍然报错
════════ 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
...
  1. 如果我们不使用MoviesBlocView
    • 在创建MoviesBloc 时尝试调用getMoviesWithGenre 时,控制台会报错。奇怪的是现在它有一个不同的错误日志
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
  • 当我们在创建MoviesBloc 时不调用getMoviesWithGenre 时没有错误,但是这没有任何意义,因为如果我们不做这样的级联,那么我不知道该怎么做例如在创建时获取请求

感谢任何帮助!

更新

经过一些调试,我发现这是因为 TabBar 的奇怪行为。它会尝试预先加载最近的选定选项卡或其他东西,然后在同一个选项卡中连续处理它,而不是一次,两次。

正如 Remi 指出的,我需要验证我的 MoviesBloc 在处理它时不会调用 notifyListeners(),在我将此验证添加到我的 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 的行为,因为我在创建时调用了该函数,所以我永远无法想象它会在它被释放后调用该函数。

I/flutter ( 1431): MoviesBloc - getMoviesWithGenre: 16
I/flutter ( 1431): MoviesBloc - getMoviesWithGenre: 16
I/flutter ( 1431): MoviesBloc - getMoviesWithGenre: 17 --> selected tab
I/flutter ( 1431): MoviesBloc - getMoviesWithGenre: 19
I/flutter ( 1431): MoviesBloc - getMoviesWithGenre: 20 --> selected tab
I/flutter ( 1431): MoviesBloc - getMoviesWithGenre: 19
I/flutter ( 1431): MoviesBloc - getMoviesWithGenre: 8
I/flutter ( 1431): MoviesBloc - getMoviesWithGenre: 7 --> selected tab
I/flutter ( 1431): MoviesBloc - getMoviesWithGenre: 8

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

【问题讨论】:

  • 在您的_setState 内部验证通知程序在调用notifyListeners 之前未被处理
  • @RémiRousselet 如你所说,notifyListeners() 似乎是在_setState(BlocState.loading) 中的 dispose() 之后调用的,然后我遵循了这个github.com/rrousselGit/provider/issues/78,它在更新#2 中运行良好。但是,当我尝试使用更新#1 中指出的MoviesBlocView 时,错误仍然会发生。您能否详细说明为什么会发生这种情况,因为为了清楚起见,MoviesBlocView 只是一个包装类?

标签: flutter provider flutter-change-notifier


【解决方案1】:

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

【讨论】:

  • 你能解释一下为什么我必须使用value而不是create吗?
  • 抱歉,忘记回答了。默认构造函数假定您确实在那里创建了新对象,因此稍后当 ChangeNotifierProvider 消失时,它将自动对其调用 dispose。所以它会在你的集团在其他地方还活着的时候处理掉它。
【解决方案2】:

重写@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();

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-06-06
    • 1970-01-01
    • 1970-01-01
    • 2018-11-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-05-10
    相关资源
    最近更新 更多