Flutter中如何使用Provider和TabBarViews动态生成表单避免脏元素?

How to avoid dirty elements using Provider and TabBarViews to dynamically generate forms in Flutter?

我对 Flutter 比较陌生,在这个应用程序中,我正在制作一个多选项卡清单表单,该表单是根据从我服务器中的 API 获取的 JSON 构建的。

为了存储此信息,我使用了一些带有 Provider 的模型,并根据与此处无关的用户输入,在预览屏幕中收到来自服务器的响应后立即更新它们。它看起来像这样:

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

我如何在主函数中设置 ChangeNotifierProvider:

  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(...);

最后是我如何修改我的 KitsEnfermagemList class,其中包含我用来呈现表单的数据:

  // *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
  }

我在动态定义需要为表单呈现的选项卡数量时遇到了一些麻烦,因此我尝试使用 Provider 作为状态管理器并在我的 TabController 之前实例化,为此我覆盖了 didChangeDependencies 方法。

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

我遇到的问题是在我渲染上一个小部件的第一刻:

======== 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]
  ...
====================================================================================================

我已经尝试使用 TickerProviderStateMixin 而不是 SingleTickerProviderStateMixin 并且它停止显示错误,但也引入了一些意外行为,例如如果我在跳到下一个,所以我断定不是解决问题,只是隐藏问题...

我已经构建了另一种不需要状态管理的非动态形式,并且工作正常。但老实说,这是我第一次尝试使用 Provider,我觉得我缺少一些实现规则。有人可以帮忙吗?

将您的 SingleTickerProviderStateMixin 替换为 TickerProviderStateMixin

这是因为重建选项卡控制器以更改选项卡计数会导致同时出现多个控制器。每个控制器都依赖于具有 vsync 值的 Ticker。所以应该有多个 Tickers 来支持它。


另注:

像这样使用 didChangeDependencies 生命周期方法会导致对您的提供者进行不必要的调用。 Because this method is called right after initState AND when a dependency of the state changes. 如果你像我猜的那样使用它来避免使用 context 并希望其中的代码只执行一次,我建议你这样使用它:

bool _isInit = true;

@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);
  }
  _isInit = false;
}

如果你真的想在每次依赖变化时执行代码,那么你可以使用 didUpdateWidget lifecycle method.