【问题标题】:How to use provider to create a commit/discard changes pattern?如何使用提供者创建提交/放弃更改模式?
【发布时间】:2021-04-12 05:08:55
【问题描述】:

什么是 Flutter 中模态小部件(基于提供者)状态管理的最佳实践,其中当用户进行编辑时,更改不会传播到父页面,直到用户确认/关闭模态小部件。 (可选)用户可以选择放弃更改。

简而言之:

  • 具有确定和取消操作的模态小部件,或
  • 模态小部件在模态关闭时应用更改

目前,我的解决方案如下所示

  1. 创建当前状态的副本
  2. 调用 Flutter 的 show___() 函数并使用提供程序(使用 .value 构造函数)包装小部件以公开状态副本
  3. 如果需要,在模态小部件关闭时更新原始状态

案例#2的例子:

Future<void> showEditDialog() async {
  // Create a copy of the current state
  final orgState = context.read<MeState>();
  final tmpState = MeState.from(orgState);

  // show modal widget with new provider
  await showDialog<void>(
    context: context,
    builder: (_) => ChangeNotifierProvider<MeState>.value(
              value: tmpState,
              builder: (context, _) => _buildEditDialogWidgets(context)),
  );

  // update original state (no discard option to keep it simple)
  orgState.update(tmpState);
}

但这也有问题,比如:

  • 我应该在哪里处理 tmpState?
  • ProxyProvider 没有 .value 构造函数。
  • 如果在 Provider 的 create: 中创建了临时状态,当模态关闭时如何安全地访问该临时状态?

更新:在我当前的应用程序中,我在小部件树的顶部有一个 MultiProvider 小部件,它创建并提供多个过滤器状态对象。例如。 FooFiltersState、BarFiltersState 和 BazFiltersState。它们是单独的类,因为这三个扩展了 ToggleableCollection&lt;T&gt; extends ChangeNotifierToggleableCollectionPickerState&lt;T&gt; extends ToggleableCollection&lt;T&gt; 类。具有通用属性和功能的抽象基类(如bool areAllSelected()toggleAllSelection() 等)。

还有一个 FiltersState extends ChangeNotifier 类,其中包含 activeFiltersCount,一个取决于 Foo、Bar 和 Baz 过滤器状态的值。这就是我使用的原因

ChangeNotifierProxyProvider3<
                FooFiltersState,
                BarFilterState,
                BazFilterState,
                FiltersState>

提供 FiltersState 实例。

用户可以通过打开模态底部工作表来编辑这些过滤器,但对过滤器的更改不得反映在应用程序中,直到通过在稀松布上点击关闭底部工作表。编辑时可以在底部工作表上看到更改。

Foo 过滤器在底部工作表上显示为筹码。 Bar 和 baz 过滤器在嵌套对话框窗口(从底部打开)中进行编辑。在编辑 Bar 或 Baz 过滤器集合时,更改必须仅反映在嵌套对话窗口内。确认嵌套对话框后,更改现在会反映在底部工作表上。如果嵌套对话框被取消,更改不会转移到底部工作表。与以前一样,这些更改在底部工作表关闭之前在应用内不可见。

为避免不必要的小部件重建,选择器小部件用于显示过滤器值。

从与 yellowgray 的讨论中,我认为我应该将所有非依赖值移出代理提供程序。因此,临时代理提供者可以创建完全独立于原始状态对象的新临时状态对象。而对于其他对象,临时状态是从原始状态构建的,并像上面的示例一样传递给值构造函数。

【问题讨论】:

    标签: flutter flutter-provider state-management flutter-state


    【解决方案1】:

    1.我应该在哪里处理 tmpState?

    我认为对于您的情况,您无需担心。 tmpState 就像函数 showEditDialog()

    内部的一个临时变量

    2. ProxyProvider 没有 .value 构造函数。

    不需要,因为它已经是。 ProxyProvider:T是需要监听的提供者。在您的情况下,它是 orgState。但是我认为orgState不会改变这个函数之外的值,所以我不知道你为什么需要它。

    3.如果在 Provider 的 create: 中创建了临时状态,那么当模态关闭时如何安全地访问该临时状态?

    您仍然可以访问 _buildEditDialogWidgets 内的 orgState 并通过 context.read() 对其进行更新。但我认为你不应该在同一个提供程序树中使用相同的类型两次 (MeState)


    实际上,当我第一次看到您的代码时,我会想为什么您需要将 tmpState 包装为另一个提供程序(您的 _buildEditDialogWidgets 包含更复杂的子树或其他需要在许多不同小部件中使用该值的东西?)。这是我能想到的更简单的版本。

    Future<void> showEditDialog() async {
     // Create a copy of the current state
     final orgState = context.read<MeState>();
    
     // show modal widget with new provider
     await showDialog<void>(
       context: context,
       builder: (_) => _buildEditDialogWidgets(context,MeState.from(orgState)),
     );
    }
    
    ...
    
    Widget _buildEditDialogWidgets(context, model){
    
      ...
      onSubmit(){
        context.read<MeState>().update(updatedModel)
      }
      ...
    }
    

    【讨论】:

    • 2.在实际代码中,我不是只使用一个提供者和一个状态对象(我简化了示例),而是使用 MultiProvider 和多个提供者和多个状态类。其中之一是 ProxyProvider,它包含 activeFiltersCount 属性并依赖于其他提供者来计算它。因此,在我的 ChangeNotifierProxyProviderValue3 的情况下(为模态构建临时状态时)T 不是 orgState1,而是 tmpState1...
    • 2.续:问题在于 temp R 状态,因为它应该包括原始 R 状态的其他属性。但是,您给了我一个想法,将 ProxyProvider 的 R 状态拆分为两个状态类。其中一个仅包含 activeFiltersCount,因此临时 ProxyProvider 的值不依赖于原始 R。然后通过普通提供程序提供另一个类。
    • 3.在 _buildEditDialogWidgets 内部,我并不总是知道模式何时关闭。例如,在 showModalBottomSheet 的情况下,如果底部工作表是通过用户在稀松布上用胶带关闭的。
    • 更简单的版本不起作用,因为final orgState = context.read&lt;MeState&gt;(); 不会创建副本,而是返回对当前状态的引用。此外,不建议通过参数将状态传递到复杂的小部件树中,因为它会产生维护问题。从临时状态创建新提供程序的优点是在 _buildEditDialogWidgets 内部无需更改任何内容。内部的小部件不知道它们正在读取/更新状态类的临时副本。
    • 我更新了更简单的版本。我没有注意到它仅通过参考,您是对的。
    【解决方案2】:

    最简单的方法是您可以在弹出对话框时提供result,并在更新提供程序时使用result

    import 'dart:collection';
    import 'dart:math';
    
    import 'package:flutter/material.dart';
    import 'package:provider/provider.dart';
    
    void main() {
      runApp(MyApp());
    }
    
    class Item {
      Item(this.name);
      String name;
    
      Item clone() => Item(name);
    }
    
    class MyState extends ChangeNotifier {
      List<Item> _items = <Item>[];
    
      UnmodifiableListView<Item> get items => UnmodifiableListView<Item>(_items);
    
      void add(Item item) {
        if (item == null) {
          return;
        }
        _items.add(item);
        notifyListeners();
      }
    
      void update(Item oldItem, Item newItem) {
        final int indexOfItem = _items.indexOf(oldItem);
        if (newItem == null || indexOfItem < 0) {
          return;
        }
        _items[indexOfItem] = newItem;
        notifyListeners();
      }
    }
    
    class MyApp extends StatelessWidget {
      @override
      Widget build(_) {
        return ChangeNotifierProvider<MyState>(
          create: (_) => MyState(),
          builder: (_, __) => MaterialApp(
            theme: ThemeData(
              primarySwatch: Colors.blue,
            ),
            home: Builder(
              builder: (BuildContext context) => Scaffold(
                body: SafeArea(
                  child: Column(
                    children: <Widget>[
                      FlatButton(
                        onPressed: () => _addItem(context),
                        child: const Text('Add'),
                      ),
                      Expanded(
                        child: Consumer<MyState>(
                          builder: (_, MyState state, __) {
                            final List<Item> items = state.items;
    
                            return ListView.builder(
                              itemCount: items.length,
                              itemBuilder: (_, int index) => GestureDetector(
                                onTap: () => _updateItem(context, items[index]),
                                child: ListTile(
                                  title: Text(items[index].name),
                                ),
                              ),
                            );
                          },
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ),
        );
      }
    
      Future<void> _addItem(BuildContext context) async {
        final Item item = await showDialog<Item>(
          context: context,
          builder: (BuildContext context2) => AlertDialog(
            actions: <Widget>[
              FlatButton(
                onPressed: () => Navigator.pop(context2),
                child: const Text('Cancel'),
              ),
              FlatButton(
                onPressed: () => Navigator.pop(
                  context2,
                  Item('New Item ${Random().nextInt(100)}'),
                ),
                child: const Text('ADD'),
              ),
            ],
          ),
        );
    
        Provider.of<MyState>(context, listen: false).add(item);
      }
    
      Future<void> _updateItem(BuildContext context, Item item) async {
        final Item updatedItem = item.clone();
        final Item tempItem = await showModalBottomSheet<Item>(
          context: context,
          builder: (_) {
            final TextEditingController controller = TextEditingController();
            controller.text = updatedItem.name;
    
            return Container(
              height: 300,
              child: Column(
                children: <Widget>[
                  Text('Original: ${item.name}'),
                  TextField(
                    controller: controller,
                    enabled: false,
                  ),
                  TextButton(
                    onPressed: () {
                      updatedItem.name = 'New Item ${Random().nextInt(100)}';
                      controller.text = updatedItem.name;
                    },
                    child: const Text('Change name'),
                  ),
                  TextButton(
                    onPressed: () => Navigator.pop(context, updatedItem),
                    child: const Text('UPDATE'),
                  ),
                  TextButton(
                    onPressed: () => Navigator.pop(context, Item(null)),
                    child: const Text('Cancel'),
                  ),
                ],
              ),
            );
          },
        );
    
        if (tempItem != null && tempItem != updatedItem) {
          // Do not update if "Cancel" is pressed.
          return;
        }
    
        // Update if "UPDATE" is pressed or dimissed.
        Provider.of<MyState>(context, listen: false).update(item, updatedItem);
      }
    }
    

    【讨论】:

    • 但是我如何提供结果,例如在 showModalBottomSheet 的情况下,如果底部工作表是通过用户在稀松布上用胶带关闭的?
    • @zigzag 你的意思是当底部的工作表被解雇时,对吧?如果是,您想要一个不等于 NULL 的结果吗?
    • 两个问题都是。
    • @zigzag 我更新了我的示例代码。请参考_updateItem(...)。足以回答您的问题吗?
    • 该答案的问题是,在 showModalBottomSheet 中,您不再使用 Provider 访问状态,而是直接传递对状态的引用。不建议通过参数将状态传递到复杂的小部件树中,因为它会产生维护问题。
    猜你喜欢
    • 2021-07-30
    • 2022-06-10
    • 2013-04-15
    • 1970-01-01
    • 2011-02-15
    • 1970-01-01
    • 2017-06-26
    • 2015-01-11
    • 1970-01-01
    相关资源
    最近更新 更多