如何使用提供程序创建 commit/discard 更改模式?

How to use provider to create a commit/discard changes pattern?

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

简而言之:

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

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

但这有一些问题,例如:

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

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

ChangeNotifierProxyProvider3<
                FooFiltersState,
                BarFilterState,
                BazFilterState,
                FiltersState>

提供 FiltersState 实例。

用户可以通过打开模式底部 sheet 来编辑这些过滤器,但是在底部 sheet 通过轻敲稀松布关闭之前,对过滤器的更改不得反映在应用程序中。编辑时底部 sheet 会显示更改。

Foo 过滤器在底部显示为筹码 sheet。 bar 和 baz 过滤器在嵌套对话框 windows(从底部打开 sheet)中编辑。编辑 Bar 或 Baz 过滤器集合时,更改必须仅反映在嵌套对话框中 window。确认嵌套对话框后,更改现在会反映在底部 sheet。如果取消嵌套对话框,更改不会转移到底部 sheet。和以前一样,在关闭底部 sheet 之前,这些更改在应用程序内部是不可见的。

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

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

最简单的方法是,您可以在弹出对话框时提供一个 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);
  }
}

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)
  }
  ...
}