2

Imagine a scenario like this.

E.g: A whole bunch of movies with various genres. The idea is a TabBar and in each Tab contains a list movie of a genre. In this case, a ChangeNotifier e.g MoviesBloc will pretty much suit the need, ChangeNotifier has nothing to with Genre. In every TabBarView child, using ChangeNotifierProvider.value is wrong here because each tab needs to hold it's own MoviesBloc state, so I will provide a ChangeNotifierProvider of MoviesBloc for each genre and then a Consumer to listen to it. I put them in a wrapper class called MoviesBlocView.

The result: - If swiping each tab sequentially, no error. - If swiping for a large number of tabs. E.g: suddenly from the 1st tab to last tab (e.g: have 20 tabs), the console will complain about reusing disposed ChangeNotifier although each tab is created with its own ChangeNotifier separately.

Code for reproduce

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

Update:

  1. If we use wrapper MoviesBlocView, whether invoking getMoviesWithGenre on MoviesBloc creation or not, the console still complains
════════ 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. If we don't use MoviesBlocView,
    • console will complain when trying to invoke getMoviesWithGenre on MoviesBloc creation. Weird that now it has a different error log
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
  • no error when we don't invoke getMoviesWithGenre on MoviesBloc creation, but that doesn't make any sense because if we don't do cascading like that, then I don't know how to do for example fetching request on creation.

Any help is appreciated!

Update

After some debugging, I found out that's because of the weird behavior of TabBar. It tries to pre-load the nearest selected tab as well or something and then dispose of it, not one, two times in a row in the same Tab.

As Remi pointed out that I need to verify my MoviesBloc doesn't invoke notifyListeners() when it is being disposed, after I added this validation to my MoviesBloc it working fine

  bool _mounted = true;
  bool get mounted => _mounted;

  @override
  void dispose() {
    _mounted = false;
    super.dispose();
  }

  void _setState(BlocState state) {
    if (!mounted) return;
    _state = state;
    notifyListeners();
  }

But this is not what I expected the behavior of MoviesBloc since I invoke the function on creation so that I will never imagine it will invoke the function after it is being disposed.

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

At this point, I really don't know what hell is going in with TabBar & TabBarView. If anyone can explain why TabBar does that I will be very appreciated!

Joe Ng
  • 91
  • 2
  • 7
  • Verify inside your `_setState` that the notifier wasn't disposed before calling `notifyListeners` – Rémi Rousselet Apr 22 '20 at 11:55
  • @RémiRousselet It seems the notifyListeners() was called after dispose() in _setState(BlocState.loading) as you said, then I followed this https://github.com/rrousselGit/provider/issues/78 and it's working fine in Update #2. But when I try to use `MoviesBlocView` as pointed out in Update #1, the error still happens. Can you elaborate on why this happens because `MoviesBlocView` is just a wrapper class for the sake of clarity? – Joe Ng Apr 22 '20 at 13:58

2 Answers2

2

Your provider setup is wrong: create: (context) => bloc. Use ChangeNotifierProvider<MoviesBloc>.value(value: bloc) instead.

szotp
  • 2,472
  • 1
  • 18
  • 21
  • Can you explain why I must use `value` instead of `create`? – Joe Ng Apr 22 '20 at 14:10
  • Sorry, forgot to answer. The default constructor assumes that you have really created new object there, so it will automatically call dispose on it later when ChangeNotifierProvider disappears. So it will dispose your bloc while it's stil alive somewhere else. – szotp May 08 '20 at 06:38
0

rewrite @override dispose method in MoviesBloc but don't call super.dispose()

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