-
Notifications
You must be signed in to change notification settings - Fork 61
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Best way to open a dialog #79
Comments
The way we do this kind of thing is through a Redux Epics class SnackBarEpics extends EpicClass<AppState> {
SnackBarEpics({
required this.messengerKey,
});
final GlobalKey<ScaffoldMessengerState>? messengerKey;
@override
Stream call(Stream actions, EpicStore<AppState> store) {
await for (final action in actions) {
String? notificationKey;
if (action is SuccessCreateOne<MemoResponse>) {
notificationKey = 'notifications.createMemoSuccess';
}
if (notificationKey != null) {
messengerKey!.currentState?.removeCurrentSnackBar();
messengerKey!.currentState?.showSnackBar(SnackBar(
content: Text(translate(notificationKey)),
));
}
}
}
} We pass in the GlobalKey for interacting with ScaffoldMessengerState at app startup. I don't really like the idea of having the Store layer know anything about the UI layer, but at least here we have only one EpicClass that acts as a bridge between the store and the UI, it's reasonably clean. I find this approach of listening for actions and doing something with Epics to be cleaner than having variables in the state to indicate whether to show a dialog. It's probably easier now with the |
Thanks, @MichaelMarner for your example, but I don't like having any UI stuff in the store either :( I thought about it a bit more and came to the conclusion that a callback added to an action would be nice. Here is an example: import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:redux/redux.dart';
part 'test_redux_callback.freezed.dart';
void main(List<String> arguments) {
final store = Store<AppState>(
reducer,
initialState: AppState.initialState(),
middleware: [middleware],
distinct: true,
);
store.onChange.listen(print);
store
..dispatch(CounterAction(count: 1))
..dispatch(CounterAction(count: 1))
..dispatch(CounterAction(count: 2, callback: callback))
..dispatch(CounterAction(count: 3, callback: callback))
..dispatch(CounterAction(count: 3, callback: callback))
..dispatch(CounterAction(count: 5, callback: callback));
}
void callback(int count) => print('count: $count');
typedef Callback = void Function(int count);
@freezed
class CounterAction with _$CounterAction implements MiddlewareAction, ReducerAction {
CounterAction._();
factory CounterAction({
required int count,
Callback? callback,
}) = _CounterAction;
@override
void middleware(Store<AppState> store, Next next) {
this.callback?.call(count);
next();
}
@override
AppState reduce(AppState state) {
return state.copyWith(counter: count);
}
}
@freezed
class AppState with _$AppState {
AppState._();
factory AppState({
required int counter,
}) = _AppState;
factory AppState.initialState() => AppState(
counter: 0,
);
}
abstract class MiddlewareAction {
void middleware(Store<AppState> store, Next next);
}
typedef Next = void Function();
void middleware(Store<AppState> store, action, NextDispatcher nextDispatcher) {
void next() {
nextDispatcher(action);
}
if (action is MiddlewareAction) {
action.middleware(store, next);
} else {
nextDispatcher(action);
}
}
abstract class ReducerAction {
AppState reduce(AppState state);
}
AppState reducer(AppState state, action) {
if (action is ReducerAction) {
return action.reduce(state);
}
return state;
} |
We do something similar that, we think, separates concerns. typedef ActionResult = void Function(AppAction action);
@noCopyFreezed
class Logout with _$Logout implements AppAction {
const factory Logout({
required ActionResult result,
}) = LogoutStart;
const factory Logout.successful() = LogoutSuccessful;
@Implements<ErrorAction>()
const factory Logout.error(Object error, StackTrace stackTrace) = LogoutError;
}
In the epics we have something like @singleton
class AuthEpics implements EpicClass<AppState> {
const AuthEpics({required AuthApi api}) : _api = api;
final AuthApi _api;
@override
Stream<dynamic> call(Stream<dynamic> actions, EpicStore<AppState> store) {
return combineEpics<AppState>(<Epic<AppState>>[
TypedEpic<AppState, LogoutStart>(_logoutStart).call,
])(actions, store);
}
Stream<AppAction> _logoutStart(Stream<LogoutStart> actions, EpicStore<AppState> store) {
return actions.flatMap((LogoutStart action) {
return Stream<void>.value(null)
.asyncMap((_) => _api.logOut())
.map((_) => const Logout.successful())
.onErrorReturnWith((Object error, StackTrace stackTrace) => Logout.error(error, stackTrace))
.doOnData(action.result);
});
}
}
In then in UI we have something like: class LogoutButton extends StatelessWidget {
const LogoutButton({super.key});
@override
Widget build(BuildContext context) {
return ListTile(
title: const Text('Logout'),
onTap: () {
final Store<AppState> store = StoreProvider.of<AppState>(context);
final NavigatorState navigator = Navigator.of(context);
store.dispatch(
Logout(
result: (AppAction action) {
if (action is LogoutError) {
final Object error = action.error;
final CapturedThemes themes = InheritedTheme.capture(
from: context,
to: Navigator.of(
context,
rootNavigator: true,
).context,
);
navigator.push(
DialogRoute<void>(
context: context,
themes: themes,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Error'),
content: Text('$error'),
);
},
),
);
}
},
),
);
},
);
}
}
|
I'm wondering what is the best way to open a dialog?
My scenario:
In the middleware, I'm doing some async backend tasks. Depending on the result, I want to open a dialog to display an error message or to indicate that the user needs to log in again.
When the async task completes, I send another action to change the status, such as
showErrorDialog = true
.In the UI code, I listen for this status (using https://github.com/brianegan/reselect_dart) and when it becomes
true
, I open the dialog and set this status tofalse
(so it will not be reopened).It works, but it also looks wrong to me.
Is there a better way to open a dialog only when an action is triggered?
The text was updated successfully, but these errors were encountered: