title: An annoying problem with TabBar and ChangeNotifierProvider
body: <p><strong>Imagine a scenario like this.</strong> </p>
  
  <p>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 <code>ChangeNotifier</code> e.g <code>MoviesBloc</code> will pretty much suit the need, <code>ChangeNotifier</code> has nothing to with <code>Genre</code>. 
  In every TabBarView child, using <code>ChangeNotifierProvider.value</code> is wrong here because each tab needs to hold it's own <code>MoviesBloc</code> state, so I will provide a <code>ChangeNotifierProvider</code> of <code>MoviesBloc</code> for each genre and then a <code>Consumer</code> to listen to it. I put them in a wrapper class called <code>MoviesBlocView</code>.</p>
  
  <p>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 <code>ChangeNotifier</code> although each tab is created with its own <code>ChangeNotifier</code> separately.</p>
  
  <p><strong>Code for reproduce</strong></p>
  
  <p><code>movies_bloc.dart</code></p>
  
  <pre><code>import 'package:flutter/material.dart';
  
  class MoviesBloc extends ChangeNotifier {
    List&lt;Movie&gt; _result;
    BlocState _state = BlocState.idle;
  
    Future&lt;void&gt; getMoviesWithGenre(Genre genre) async {
      _setState(BlocState.loading);
      await Future.delayed(_delayTime);
      _result = List.generate(
          20, (index) =&gt; Movie(id: index + 1, name: (index + 1).toString()));
      _setState(BlocState.loaded);
    }
  
    List&lt;Movie&gt; get result =&gt; _result;
  
    bool get loading =&gt; _state == BlocState.loading;
  
    BlocState get state =&gt; _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);
  </code></pre>
  
  <p><code>movies_bloc_view.dart</code></p>
  
  <pre><code>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&lt;Movie&gt; result) loadedBuilder;
  
    @override
    Widget build(BuildContext context) {
      return ChangeNotifierProvider&lt;MoviesBloc&gt;(
        create: (context) =&gt; bloc,
        child: Consumer&lt;MoviesBloc&gt;(
          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();
            }
          },
        ),
      );
    }
  }
  </code></pre>
  
  <p><code>main.dart</code></p>
  
  <pre><code>import 'package:flutter/material.dart';
  import 'package:provider/provider.dart';
  
  import 'movies_bloc.dart';
  import 'movies_bloc_view.dart';
  
  List&lt;Genre&gt; genres = List.generate(
      20, (index) =&gt; 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 &lt;Widget&gt;[
                SliverAppBar(
                  forceElevated: innerBoxIsScrolled,
                  title: const Text('Provider'),
                  bottom: TabBar(
                    isScrollable: true,
                    tabs: genres
                        .map((genre) =&gt; 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) =&gt; MoviesView(
                      movies: movies,
                      genre: genre,
                    ),
                  );
                  // or use provider directly
                  return ChangeNotifierProvider&lt;MoviesBloc&gt;(
                    create: (context) =&gt; MoviesBloc()..getMoviesWithGenre(genre),
                    child: Consumer&lt;MoviesBloc&gt;(
                      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&lt;Movie&gt; movies;
    final Genre genre;
  
    @override
    _MoviesViewState createState() =&gt; _MoviesViewState();
  }
  
  class _MoviesViewState extends State&lt;MoviesView&gt;
      with AutomaticKeepAliveClientMixin {
    @override
    bool get wantKeepAlive =&gt; 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(),
      );
    }
  }
  </code></pre>
  
  <p><strong>Update</strong>: </p>
  
  <ol>
  <li>If we use wrapper <code>MoviesBlocView</code>, whether invoking <code>getMoviesWithGenre</code> on <code>MoviesBloc</code> creation or not, the console still complains</li>
  </ol>
  
  <pre><code>════════ Exception caught by widgets library ═══════════════════════════════════
  The following assertion was thrown building Consumer&lt;MoviesBloc&gt;(dirty, dependencies: [_DefaultInheritedProviderScope&lt;MoviesBloc&gt;]):
  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&lt;MoviesBloc&gt; 
  lib\test_tabs\movies_bloc_view.dart:23
  When the exception was thrown, this was the stack
  #0      ChangeNotifier._debugAssertNotDisposed.&lt;anonymous closure&gt; 
  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
  ...
  </code></pre>
  
  <ol start="2">
  <li>If we don't use <code>MoviesBlocView</code>, 
  
  <ul>
  <li>console will complain when trying to invoke <code>getMoviesWithGenre</code> on <code>MoviesBloc</code> creation. Weird that now it has a different error log</li>
  </ul></li>
  </ol>
  
  <pre><code>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.&lt;anonymous closure&gt;[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): &lt;asynchronous suspension&gt;
  [38;5;248mE/flutter (30300): #5      HomePage.build.&lt;anonymous closure&gt;.&lt;anonymous closure&gt;[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.&lt;anonymous closure&gt;[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): &lt;asynchronous suspension&gt;
  [38;5;248mE/flutter (30300): #5      HomePage.build.&lt;anonymous closure&gt;.&lt;anonymous closure&gt;[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
  </code></pre>
  
  <ul>
  <li>no error when we don't invoke <code>getMoviesWithGenre</code> on <code>MoviesBloc</code> creation, but <strong>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</strong>.</li>
  </ul>
  
  <p>Any help is appreciated!</p>
  
  <p><strong>Update</strong></p>
  
  <p>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.</p>
  
  <p>As Remi pointed out that I need to verify my <code>MoviesBloc</code> doesn't invoke notifyListeners() when it is being disposed, after I added this validation to my <code>MoviesBloc</code> it working fine</p>
  
  <pre><code>  bool _mounted = true;
    bool get mounted =&gt; _mounted;
  
    @override
    void dispose() {
      _mounted = false;
      super.dispose();
    }
  
    void _setState(BlocState state) {
      if (!mounted) return;
      _state = state;
      notifyListeners();
    }
  </code></pre>
  
  <p>But this is not what I expected the behavior of <code>MoviesBloc</code> since I invoke the function on creation so that I will never imagine it will invoke the function after it is being disposed.</p>
  
  <pre><code>I/flutter ( 1431): MoviesBloc - getMoviesWithGenre: 16
  I/flutter ( 1431): MoviesBloc - getMoviesWithGenre: 16
  I/flutter ( 1431): MoviesBloc - getMoviesWithGenre: 17 --&gt; selected tab
  I/flutter ( 1431): MoviesBloc - getMoviesWithGenre: 19
  I/flutter ( 1431): MoviesBloc - getMoviesWithGenre: 20 --&gt; selected tab
  I/flutter ( 1431): MoviesBloc - getMoviesWithGenre: 19
  I/flutter ( 1431): MoviesBloc - getMoviesWithGenre: 8
  I/flutter ( 1431): MoviesBloc - getMoviesWithGenre: 7 --&gt; selected tab
  I/flutter ( 1431): MoviesBloc - getMoviesWithGenre: 8
  </code></pre>
  
  <p>At this point, I really don't know what hell is going in with TabBar &amp; TabBarView. If anyone can explain why TabBar does that I will be very appreciated!</p>