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.