Досадная проблема с TabBar и ChangeNotifierProvider

Представьте себе такой сценарий.

Например: Целая куча фильмов разных жанров. Идея заключается в TabBar, и в каждой вкладке содержится список фильмов жанра. В этом случае ChangeNotifier, например, MoviesBloc в значительной степени подойдет, ChangeNotifier не имеет ничего общего с Genre. В каждом дочернем элементе TabBarView использование ChangeNotifierProvider.value здесь неверно, потому что каждая вкладка должна содержать собственное MoviesBloc состояние, поэтому я предоставлю ChangeNotifierProvider из MoviesBloc для каждого жанра, а затем Consumer, чтобы слушать его. Я помещаю их в класс-оболочку под названием MoviesBlocView.

Результат: - Если пролистывать каждую вкладку последовательно, ошибок нет. - При пролистывании большого количества вкладок. Например: внезапно с 1-й вкладки на последнюю (например, есть 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, независимо от того, вызывается ли getMoviesWithGenre при создании MoviesBloc или нет, консоль все равно жалуется
════════ 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
  • нет ошибки, когда мы не вызываем getMoviesWithGenre при MoviesBloc создании, но это не имеет никакого смысла, потому что если мы не делаем каскадирование таким образом, то я не знаю, как это сделать, например, получить запрос при создании .

Любая помощь приветствуется!

Обновить

После некоторой отладки я обнаружил, что это из-за странного поведения TabBar. Он пытается предварительно загрузить ближайшую выбранную вкладку или что-то еще, а затем избавиться от нее, а не один, два раза подряд в той же вкладке.

Как заметил Реми, мне нужно убедиться, что мой 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 это делает, я буду очень признателен!


person Joe Ng    schedule 22.04.2020    source источник
comment
Перед вызовом notifyListeners проверьте внутри своего _setState, что средство уведомления не было удалено.   -  person Rémi Rousselet    schedule 22.04.2020
comment
@ RémiRousselet Кажется, что notifyListeners () был вызван после dispose () в _setState (BlocState.loading), как вы сказали, затем я последовал этому github.com/rrousselGit/provider/issues/78, и он отлично работает в обновлении №2. Но когда я пытаюсь использовать MoviesBlocView, как указано в обновлении № 1, ошибка все равно возникает. Не могли бы вы пояснить, почему это происходит, потому что MoviesBlocView - это просто класс-оболочка для ясности?   -  person Joe Ng    schedule 22.04.2020


Ответы (2)


Ваш провайдер настроен неверно: create: (context) => bloc. Вместо этого используйте ChangeNotifierProvider<MoviesBloc>.value(value: bloc).

person szotp    schedule 22.04.2020
comment
Вы можете объяснить, почему я должен использовать value вместо create? - person Joe Ng; 22.04.2020
comment
Простите, забыл ответить. Конструктор по умолчанию предполагает, что вы действительно создали там новый объект, поэтому он автоматически вызовет dispose для него позже, когда ChangeNotifierProvider исчезнет. Так что он утилизирует ваш блок, пока он еще жив, в другом месте. - person szotp; 08.05.2020

переписать метод @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();
person Ballyees    schedule 10.01.2021