1

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

简而言之:

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

目前,我的解决方案看起来像这样

  1. 创建当前状态的副本
  2. 调用颤振的 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<T> extends ChangeNotifierToggleableCollectionPickerState<T> extends ToggleableCollection<T>类。具有通用属性和功能(如 等)的抽象基bool areAllSelected()toggleAllSelection()

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

ChangeNotifierProxyProvider3<
                FooFiltersState,
                BarFilterState,
                BazFilterState,
                FiltersState>

提供 FiltersState 实例。

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

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

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

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

4

2 回答 2

2

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

我认为对于您的情况,您不必担心。tmpState 就像函数showEditDialog()中的一个临时变量

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

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

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

您仍然可以访问orgState内部_buildEditDialogWidgets并通过 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)
  }
  ...
}
于 2021-02-02T07:46:23.287 回答
1

最简单的方法是您可以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);
  }
}
于 2021-02-02T07:43:40.560 回答