What would be a best practice for (provider based) state management of modal widgets in flutter, where when user makes an edit changes do not propagate to parent page until user confirms/closes modal widget. Optionally, user has a choice to discard the changes.
In a nutshell:
Currently, my solution looks like this
Example of case #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);
}
But there are issues with this, like:
create:
instead, how can I safely access that temporary state when modal is closed?UPDATE: In my current app I have a MultiProvider widget at the top of widget tree, that creates and provides multiple filter state objects. Eg. FooFiltersState, BarFiltersState and BazFiltersState. They are separate classes because each these three extends either ToggleableCollection<T> extends ChangeNotifier
or ToggleableCollectionPickerState<T> extends ToggleableCollection<T>
class. An abstract base classes with common properties and functions (like bool areAllSelected()
, toggleAllSelection()
etc.).
There is also FiltersState extends ChangeNotifier
class that contains among other things activeFiltersCount
, a value depended on Foo, Bar and Baz filters state. That's why I use
ChangeNotifierProxyProvider3<
FooFiltersState,
BarFilterState,
BazFilterState,
FiltersState>
to provide FiltersState instance.
User can edit these filters by opening modal bottom sheet, but changes to filters must not be reflected in the app until bottom sheet is closed by taping on the scrim. Changes are visible on the bottom sheet while editing.
Foo filters are displayed as chips on the bottom sheet. Bar and baz filters are edited inside a nested dialog windows (opened from the bottom sheet). While Bar or Baz filter collection is edited, changes must be reflected only inside the nested dialog window. When nested dialog is confirmed changes are now reflected on bottom sheet. If nested dialog is canceled changes are not transferred to the bottom sheet. Same as before, these changes are not visible inside the app until the bottom sheet is closed.
To avoid unnecessary widget rebuilds, Selector widgets are used to display filter values.
From discussion with yellowgray, I think that I should move all non-dependent values out of proxy provider. So that, temp proxy provider can create new temp state object that is completely independent of original state object. While for other objects temp states are build from original states and passed to value constructors like in the above example.
1. Where should I dispose tmpState?
I think for your case, you don't need to worry about it. tmpState is like a temporary variabl inside function showEditDialog()
2. ProxyProvider doesn't have .value constructor.
It doesn't need to because it already is. ProxyProvider<T, R>: T is a provider that need to listen to. In your case it is the orgState. But I think the orgState won't change the value outside of this function, so I don't know why you need it.
3. If temporary state is created in Provider's create: instead, how can I safely access that temporary state when modal is closed?
you can still access the orgState
inside _buildEditDialogWidgets
and update it by context.read(). But I think you shouldn't use same type twice in the same provider tree (MeState)
Actually when I first see your code, I will think why you need to wrap tmpState as another provider (your _buildEditDialogWidgets contains more complicated sub-tree or something else that need to use the value in many different widget?). Here is the simpler version I can think of.
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)
}
...
}