Home > front end >  How to avoid dirty elements using Provider and TabBarViews to dynamically generate forms in Flutter?
How to avoid dirty elements using Provider and TabBarViews to dynamically generate forms in Flutter?

Time:04-29

I'm relatively new to Flutter and in this app I'm making a multiple tab checklist form built from a JSON fetched from an API in my server.

To store this info I'm using a few models with Provider and updating them as soon as I get the response from the server in a preview screen, based on an input from the user that's not relevant here. It looks like this:

**The models**

class KitsEnfermagemList with ChangeNotifier {
  List kits;

  KitsEnfermagemList({required this.kits});

  // a bunch of methods

  void addOrUpdate(KitEnfermagem newKit) {
    if (doesNotExists(newKit)) {
      kits.add(newKit);
      notifyListeners();
    } else {
      KitEnfermagem oldKit = find(newKit)[0];
      oldKit.update(newKit);
    }
  }

  void updateAllFromJson(List<dynamic> json) {
    if (kits.isNotEmpty) {
      for (int i = 0; i < json.length; i  ) {
        var newKit = KitEnfermagem.fromJson(json[i]);
        addOrUpdate(newKit);
      }
    } else {
      addMultiple(json.map((kit) => KitEnfermagem.fromJson(kit)).toList());
    }
    return;
  }

class KitEnfermagem with ChangeNotifier {
  String? id;
  String? compartimento;
  int? tipoVeiculo;
  List<ItemPerKit>? itens;

  KitEnfermagem({
    this.id,
    this.compartimento,
    this.tipoVeiculo,
    this.itens,
  });

  void update(KitEnfermagem newKit) {
    compartimento = newKit.compartimento;
    tipoVeiculo = newKit.tipoVeiculo;
    itens = newKit.itens;
    notifyListeners();
  }
}

How I setup my ChangeNotifierProvider in my main function:

  Widget build(BuildContext context) {
    SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);

    return MultiProvider(
      providers: [
        ChangeNotifierProvider<Veiculo>(create: (ctx) => Veiculo()),
        ChangeNotifierProvider<KitEnfermagem>(create: (ctx) => KitEnfermagem()),
        ChangeNotifierProvider<KitsEnfermagemList>(
            create: (ctx) => KitsEnfermagemList(kits: [])),
      ],
      child: MaterialApp(...);

And finally how I'm modifying my KitsEnfermagemList class, which contains the data that I'm using to render the form:

  // *I'm instantiating this provider on my build(context) function and passing it as a parameter of _submitForm function, just to be clear.*
  final kitsEnfermagemList = Provider.of<KitsEnfermagemList>(context);

  _submitForm(KitsEnfermagemList kitsEnfermagemList) async {
   // do stuff
      try {
      var responseJson = await _apiService.get(_endpoint);
      setState(() {
        _isLoading = false;
      });
     **kitsEnfermagemList.updateAllFromJson(responseJson);
      Navigator.of(context)
          .pushReplacementNamed(Routes.checklistEnfermagemForm);**
    } on... // handling errors
  }

I was having some trouble defining dynamically the amount of tabs I would need to render for the form, so I tried to use Provider as a state manager and instantiate before my TabController, and to do so I overrode didChangeDependencies method.

class _ChecklistEnfermagemFormViewState
    extends State<ChecklistEnfermagemFormView>
    with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin {

  @override
  bool get wantKeepAlive => true;

  int currentTab = 0;
  final Map<String, dynamic> _formData = {'checklists_kit': []};
  late TabController _tabController;
  late Veiculo veiculo;
  late KitsEnfermagemList kitsEnfermagemList;
  late int tabsLength;

    @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    veiculo = Provider.of<Veiculo>(context);
    kitsEnfermagemList = Provider.of<KitsEnfermagemList>(context);
    assert(kitsEnfermagemList.isNotEmpty == true);
    tabsLength = kitsEnfermagemList.length   viewModel.othersTabsLength;
    kitsEnfermagemList.kits
        .map((kit) => _formData['checklists_kit'].add(ChecklistKit().toJson()));
    _tabController = TabController(length: tabsLength, vsync: this);
  }

    TabBarView _buildTabBarView() {
      return TabBarView(
        physics: const NeverScrollableScrollPhysics(),
        controller: _tabController,
        children: <Widget>[
          // dynamically generating each tab
        ],
      ),
    );

    return CustomWillPopScope(
      child: Scaffold(
        appBar: appBar,
        body: AppBackground(
          child: _isLoading
              ? const Center(child: CircularProgressIndicator())
              : _buildTabBarView(),
        ),
      ),
    );
  }
}

And the problem I'm getting is this right on the first moment I render the previous widget:

======== Exception caught by widgets library =======================================================
The following assertion was thrown while rebuilding dirty elements:
_ChecklistEnfermagemFormViewState is a SingleTickerProviderStateMixin but multiple tickers were created.

A SingleTickerProviderStateMixin can only be used as a TickerProvider once.

If a State is used for multiple AnimationController objects, or if it is passed to other objects and those objects might use it more than one time in total, then instead of mixing in a SingleTickerProviderStateMixin, use a regular TickerProviderStateMixin.

The relevant error-causing widget was: 
  ChecklistEnfermagemFormView ChecklistEnfermagemFormView:file:///mnt/5dfbbeb6-7a4a-4a3a-9f0d-dd39eb411d55/Projetos/_mobile/dirigir_assessoria/lib/main.dart:110:21
When the exception was thrown, this was the stack: 
#0      SingleTickerProviderStateMixin.createTicker.<anonymous closure> (package:flutter/src/widgets/ticker_provider.dart:188:7)
#1      SingleTickerProviderStateMixin.createTicker (package:flutter/src/widgets/ticker_provider.dart:197:6)
#2      new AnimationController.unbounded (package:flutter/src/animation/animation_controller.dart:279:21)
#3      new TabController (package:flutter/src/material/tab_controller.dart:110:50)
#4      _ChecklistEnfermagemFormViewState.didChangeDependencies (package:dirigir_assessoria/screens/enfermagem/checklist_enfermagem_form_screen.dart:159:22)
#5      StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:4925:13)
#6      Element.rebuild (package:flutter/src/widgets/framework.dart:4477:5)
#7      BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2659:19)
#8      WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:882:21)
#9      RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:363:5)
#10     SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1144:15)
#11     SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1081:9)
#12     SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:995:5)
#16     _invoke (dart:ui/hooks.dart:151:10)
#17     PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:308:5)
#18     _drawFrame (dart:ui/hooks.dart:115:31)
(elided 3 frames from dart:async)
The element being rebuilt at the time was index 3 of 7: ChecklistEnfermagemFormView
  dirty
  dependencies: [_InheritedTheme, MediaQuery, _InheritedProviderScope<KitsEnfermagemList>, _LocalizationsScope-[GlobalKey#a34b4], _InheritedProviderScope<Veiculo>]
  state: _ChecklistEnfermagemFormViewState#53df3(ticker inactive)
====================================================================================================

======== Exception caught by scheduler library =====================================================
buildScope missed some dirty elements.
The list of dirty elements at the end of the buildScope call was: 
  : HomeScreen
    dependencies: [_InheritedTheme, MediaQuery, _LocalizationsScope-[GlobalKey#a34b4]]
  : ChecklistEnfermagemFormView
    dirty
    dependencies: [_InheritedTheme, MediaQuery, _InheritedProviderScope<KitsEnfermagemList>, _LocalizationsScope-[GlobalKey#a34b4], _InheritedProviderScope<Veiculo>]
    state: _ChecklistEnfermagemFormViewState#53df3(ticker inactive)
  : Scaffold
    dependencies: [Directionality, _InheritedTheme, MediaQuery, _ScaffoldMessengerScope, UnmanagedRestorationScope, _LocalizationsScope-[GlobalKey#a34b4]]
    state: ScaffoldState#aa553(tickers: tracking 2 tickers)
  : Scaffold
    dependencies: [Directionality, _InheritedTheme, MediaQuery, UnmanagedRestorationScope, _ScaffoldMessengerScope, _LocalizationsScope-[GlobalKey#a34b4]]
    state: ScaffoldState#07a62(tickers: tracking 2 tickers)
  : AppBar
    dependencies: [_InheritedTheme, _ScrollNotificationObserverScope, MediaQuery, FlexibleSpaceBarSettings, _ModalScopeStatus, _LocalizationsScope-[GlobalKey#a34b4]]
    state: _AppBarState#06d17
  : Text
    "Checklist da Enfermagem"
    dependencies: [MediaQuery, DefaultTextStyle]
  : Text
    "Gaveta 1"
    debugLabel: englishLike labelLarge 2014
    inherit: false
    size: 14.0
    weight: 500
    baseline: alphabetic
    dependencies: [MediaQuery, DefaultTextStyle]
  : Text
    "Mala Laringoscópio"
    debugLabel: englishLike labelLarge 2014
    inherit: false
    size: 14.0
    weight: 500
    baseline: alphabetic
    dependencies: [MediaQuery, DefaultTextStyle]
  : Text
    "Balcão/Salão"
    debugLabel: englishLike labelLarge 2014
    inherit: false
    size: 14.0
    weight: 500
    baseline: alphabetic
    dependencies: [MediaQuery, DefaultTextStyle]
  : Text
    "Maleta Psicotrópicos"
    debugLabel: englishLike labelLarge 2014
    inherit: false
    size: 14.0
    weight: 500
    baseline: alphabetic
    dependencies: [MediaQuery, DefaultTextStyle]
  : Text
    "Baú"
    debugLabel: englishLike labelLarge 2014
    inherit: false
    size: 14.0
    weight: 500
    baseline: alphabetic
    dependencies: [MediaQuery, DefaultTextStyle]
  : Text
    "Assinatura"
    debugLabel: englishLike labelLarge 2014
    inherit: false
    size: 14.0
    weight: 500
    baseline: alphabetic
    dependencies: [MediaQuery, DefaultTextStyle]
  : Text
    "Armário A"
    debugLabel: englishLike labelLarge 2014
    inherit: false
    size: 14.0
    weight: 500
    baseline: alphabetic
    dependencies: [MediaQuery, DefaultTextStyle]
  : Text
    "Gaveta 2"
    debugLabel: englishLike labelLarge 2014
    inherit: false
    size: 14.0
    weight: 500
    baseline: alphabetic
    dependencies: [MediaQuery, DefaultTextStyle]
  : Text
    "Régua"
    debugLabel: englishLike labelLarge 2014
    inherit: false
    size: 14.0
    weight: 500
    baseline: alphabetic
    dependencies: [MediaQuery, DefaultTextStyle]
  : Text
    "Mala de Vias Aéreas e Oxigenoterapia"
    debugLabel: englishLike labelLarge 2014
    inherit: false
    size: 14.0
    weight: 500
    baseline: alphabetic
    dependencies: [MediaQuery, DefaultTextStyle]
  : Text
    "Mala de Trauma"
    debugLabel: englishLike labelLarge 2014
    inherit: false
    size: 14.0
    weight: 500
    baseline: alphabetic
    dependencies: [MediaQuery, DefaultTextStyle]
  : Text
    "Bolsa de Sinais Vitais"
    debugLabel: englishLike labelLarge 2014
    inherit: false
    size: 14.0
    weight: 500
    baseline: alphabetic
    dependencies: [MediaQuery, DefaultTextStyle]
  : Text
    "Mala de Procedimento"
    debugLabel: englishLike labelLarge 2014
    inherit: false
    size: 14.0
    weight: 500
    baseline: alphabetic
    dependencies: [MediaQuery, DefaultTextStyle]
  : Text
    "Mala de Medicação e Acesso Venoso"
    debugLabel: englishLike labelLarge 2014
    inherit: false
    size: 14.0
    weight: 500
    baseline: alphabetic
    dependencies: [MediaQuery, DefaultTextStyle]
  : Text
    "Gaveta 3"
    debugLabel: englishLike labelLarge 2014
    inherit: false
    size: 14.0
    weight: 500
    baseline: alphabetic
    dependencies: [MediaQuery, DefaultTextStyle]
  : Text
    "Armário B"
    debugLabel: englishLike labelLarge 2014
    inherit: false
    size: 14.0
    weight: 500
    baseline: alphabetic
    dependencies: [MediaQuery, DefaultTextStyle]
  : Text
    "Fotos"
    debugLabel: englishLike labelLarge 2014
    inherit: false
    size: 14.0
    weight: 500
    baseline: alphabetic
    dependencies: [DefaultTextStyle, MediaQuery]
  : Text
    "Inicial"
    debugLabel: englishLike labelLarge 2014
    inherit: false
    size: 14.0
    weight: 500
    baseline: alphabetic
    dependencies: [DefaultTextStyle, MediaQuery]
  ...
====================================================================================================

I've already tried to use TickerProviderStateMixin instead of SingleTickerProviderStateMixin and it stops showing the error, but also introduces some unexpected behaviors, such as leading back to the initial tab if I dismiss its remaining keyboard after I've jumped to the next one, so I concluded that it was not solving the problem, it was just hiding it...

I've built another form less dynamically without the need of an state management and it's working fine. But honestly this is first time I'm trying to use Provider and I feel like I'm missing some implementation rule. Can anyone help?

CodePudding user response:

Replace your SingleTickerProviderStateMixin with TickerProviderStateMixin

This is because rebuilding the tabs controller to change the tabs count causes multiple controllers to be present at once. And each controller depends on a Ticker with the vsync value. So there should be multiple Tickers to support that.


Another note:

Using the didChangeDependencies lifecycle method like this causes unnecessary calls to your provider. Because this method is called right after initState AND when a dependency of the state changes. If you're using it as I'm guessing to avoid using context and want the code inside it to execute only once, I recommend you use it like this:

bool _isInit = false;

@override
void didChangeDependencies() {
  super.didChangeDependencies();
  if(_isInit) {
    // The following code is executed only once
    veiculo = Provider.of<Veiculo>(context);
    kitsEnfermagemList = Provider.of<KitsEnfermagemList>(context);
    assert(kitsEnfermagemList.isNotEmpty == true);
    tabsLength = kitsEnfermagemList.length   viewModel.othersTabsLength;
    kitsEnfermagemList.kits
        .map((kit) => _formData['checklists_kit'].add(ChecklistKit().toJson()));
    _tabController = TabController(length: tabsLength, vsync: this);
  }
}

If you really want to execute code on every dependency change, then you can use the didUpdateWidget lifecycle method.

  • Related