Skip to content

Showing dialogs or doing navigation on displaying screen based on state data #102

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

Closed
DFreds opened this issue Feb 7, 2019 · 1 comment
Closed

Comments

@DFreds
Copy link

DFreds commented Feb 7, 2019

Use Case

I have the following use case:

  1. The user opens the app and is presented with a loading indicator
  2. Behind the scenes, the app makes a network request to determine the health of the system (is the backend up, what the allowed minimum version is, etc.)
  3. Upon completion of the request, I need to do a few things
    a. If the call failed, navigate to the login page (assume everything is okay) and clear the back stack. I have this working via dispatching a navigation action in the middleware.
    b. If the call succeeded, do our validation and show a dialog if invalid or go to the login page if valid and clear the back stack. I'm struggling with both of these cases.

I am using the logic described here for navigation. However, I'm not quite sure how to show a dialog if the data is invalid or navigate if it is valid. I need a BuildContext in order to actually use the showDialog method which makes it problematic to do in the middleware. If I put the logic in some redux container, I get the following error on trying to show the dialog.

dialog output

Restarted application in 1,856ms.
I/flutter (20134): Instance of 'LoadHealthCheckAction'
I/flutter (20134): Instance of 'HealthCheckSuccessAction'
I/flutter (20134): ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
I/flutter (20134): The following assertion was thrown building StreamBuilder<_ViewModel>(dirty, state:
I/flutter (20134): _StreamBuilderBaseState<_ViewModel, AsyncSnapshot<_ViewModel>>#6adf2):
I/flutter (20134): setState() or markNeedsBuild() called during build.
I/flutter (20134): This Overlay widget cannot be marked as needing to build because the framework is already in the
I/flutter (20134): process of building widgets. A widget can be marked as needing to be built during the build phase
I/flutter (20134): only if one of its ancestors is currently building. This exception is allowed because the framework
I/flutter (20134): builds parent widgets before children, which means a dirty descendant will always be built.
I/flutter (20134): Otherwise, the framework might not visit this widget during this build phase.
I/flutter (20134): The widget on which setState() or markNeedsBuild() was called was:
I/flutter (20134):   Overlay-[LabeledGlobalKey<OverlayState>#56055](state: OverlayState#ad67b(entries:
I/flutter (20134):   [OverlayEntry#d9200(opaque: false; maintainState: false), OverlayEntry#dba24(opaque: false;
I/flutter (20134):   maintainState: true), OverlayEntry#dd974(opaque: false; maintainState: false),
I/flutter (20134):   OverlayEntry#a603e(opaque: false; maintainState: true)]))
I/flutter (20134): The widget which was currently being built when the offending call was made was:
I/flutter (20134):   StreamBuilder<_ViewModel>(dirty, state: _StreamBuilderBaseState<_ViewModel,
I/flutter (20134):   AsyncSnapshot<_ViewModel>>#6adf2)
I/flutter (20134): 
I/flutter (20134): When the exception was thrown, this was the stack:
I/flutter (20134): #0      Element.markNeedsBuild.<anonymous closure> 
I/flutter (20134): #1      Element.markNeedsBuild 
I/flutter (20134): #2      State.setState 
I/flutter (20134): #3      OverlayState.insertAll 
I/flutter (20134): #4      OverlayRoute.install 
I/flutter (20134): #5      TransitionRoute.install 
I/flutter (20134): #6      ModalRoute.install 
I/flutter (20134): #7      NavigatorState.push 
I/flutter (20134): #8      showGeneralDialog 
I/flutter (20134): #9      showDialog 
I/flutter (20134): #10     HealthCheckContainer.build.<anonymous closure> 
I/flutter (20134): #11     _StoreStreamListenerState.build.<anonymous closure> (package:flutter_redux/flutter_redux.dart)
I/flutter (20134): #12     StreamBuilder.build 
I/flutter (20134): #13     _StreamBuilderBaseState.build 
I/flutter (20134): #14     StatefulElement.build 
I/flutter (20134): #15     ComponentElement.performRebuild 
I/flutter (20134): #16     Element.rebuild 
I/flutter (20134): #17     BuildOwner.buildScope 
I/flutter (20134): #18     _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding&PaintingBinding&SemanticsBinding&RendererBinding&WidgetsBinding.drawFrame 
I/flutter (20134): #19     _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding&PaintingBinding&SemanticsBinding&RendererBinding._handlePersistentFrameCallback 
I/flutter (20134): #20     _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding._invokeFrameCallback 
I/flutter (20134): #21     _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding.handleDrawFrame 
I/flutter (20134): #22     _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding._handleDrawFrame 
I/flutter (20134): #23     _invoke (dart:ui/hooks.dart:159:13)
I/flutter (20134): #24     _drawFrame (dart:ui/hooks.dart:148:3)
I/flutter (20134): ════════════════════════════════════════════════════════════════════════════════════════════════════

Code

The relevant code is below. Any help is greatly appreciated.

app.dart

final GlobalKey<NavigatorState> navigatorKey = new GlobalKey<NavigatorState>();

class App extends StatelessWidget {
  final Store<AppState> store;

  App({
    Key key,
    this.store,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return StoreProvider<AppState>(
      store: store,
      child: MaterialApp(
        title: "My App",
        theme: _buildTheme(),
        navigatorKey: navigatorKey,
        routes: <String, WidgetBuilder>{
          "/": (BuildContext context) {
            return StartupScreen(
              onInit: () {
                StoreProvider.of<AppState>(context).dispatch(LoadHealthCheckAction());
              }
            );
          },
          "/login": (BuildContext context) {
            return LoginScreen();
          },
        },
      ),
    );
  }

  ThemeData _buildTheme() {
    return ThemeData(
      primarySwatch: Colors.purple,
    );
  }
}

startup_screen.dart

class StartupScreen extends StatefulWidget {
  final VoidCallback onInit;

  StartupScreen({
    Key key,
    @required this.onInit,
  }) : super(key: key);

  @override
  StartupScreenState createState() {
    return StartupScreenState();
  }
}

class StartupScreenState extends State<StartupScreen> {
  @override
  void initState() {
    widget.onInit();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      body: SafeArea(
        child: Stack(
          children: <Widget>[
            Positioned(
              top: 25,
              left: 25,
              child: Image.asset(
                "assets/my_logo.png",
              ),
            ),
            HealthCheckContainer(),
          ],
        ),
      ),
    );
  }
}

health_check_container.dart

class HealthCheckContainer extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StoreConnector<AppState, _ViewModel>(
      distinct: true,
      converter: _ViewModel.fromStore,
      builder: (BuildContext context, _ViewModel viewModel) {
        if (viewModel.healthCheckData != null) {
          if (!viewModel.healthCheckData.isPlatformUp) {
            
            // TODO throws an error here
            showDialog(
              context: context,
              builder: (BuildContext context) {
                return AlertDialog(
                  title: Text('App down'),
                  content: Text('Oh noes'),
                );
              }
            );
          }

          // TODO how do I navigate to login now? This would also throw an error if I dispatched a navigation action
        }

        return CenterLoading();
      },
    );
  }
}

class _ViewModel {
  final HealthCheckData healthCheckData;

  _ViewModel({
    @required this.healthCheckData,
  });

  static _ViewModel fromStore(Store<AppState> store) {
    return _ViewModel(
      healthCheckData: store.state.healthCheckState.data,
    );
  }
}

app_middleware.dart

class AppMiddleware {
  final MyWebClient webClient;

  const AppMiddleware({
    this.webClient = const MyWebClient(),
  });

  List<Middleware<AppState>> createMiddleware() {
    return <Middleware<AppState>>[
      TypedMiddleware<AppState, dynamic>(_logAction),
      TypedMiddleware<AppState, NavigateToAction>(_navigateTo),
      TypedMiddleware<AppState, LoadHealthCheckAction>(_loadHealthCheck),
    ];
  }

  void _logAction(
    Store<AppState> store,
    dynamic action,
    NextDispatcher next,
  ) async {
    next(action);

    print(action);
  }

  void _navigateTo(
    Store<AppState> store,
    NavigateToAction action,
    NextDispatcher next,
  ) {
    next(action);

    if (action.clearStack) {
      navigatorKey.currentState.pushNamedAndRemoveUntil(action.route, (route) => false);
      return;
    }

    navigatorKey.currentState.pushNamed(action.route);
  }

  void _loadHealthCheck(
    Store<AppState> store,
    LoadHealthCheckAction action,
    NextDispatcher next,
  ) {
    next(action);

    webClient.fetchHealthCheckData().then((data) {
      store.dispatch(HealthCheckSuccessAction(data: data));
    }).catchError((error) {
      store.dispatch(HealthCheckFailureAction());
      // Successfully routing to login in this failure case
      store.dispatch(NavigateToAction(route: "/login", clearStack: true));
    });
  }
}

health_check_state.dart

@immutable
class HealthCheckState {
  final bool isLoading;
  final HealthCheckData data;
  final bool isError;

  HealthCheckState({
    @required this.isLoading,
    @required this.data,
    @required this.isError,
  });

  factory HealthCheckState.initial() {
    return HealthCheckState(
      isLoading: false,
      data: null,
      isError: false,
    );
  }

  HealthCheckState copyWith({
    bool isLoading,
    HealthCheckData data,
    bool isError,
  }) {
    return HealthCheckState(
      isLoading: isLoading ?? this.isLoading,
      data: data ?? this.data,
      isError: isError ?? this.isError,
    );
  }

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is HealthCheckState &&
          runtimeType == other.runtimeType &&
          isLoading == other.isLoading &&
          data == other.data &&
          isError == other.isError;

  @override
  int get hashCode => isLoading.hashCode ^ data.hashCode ^ isError.hashCode;
}

health_check_reducer.dart

Reducer<HealthCheckState> healthCheckReducer = combineReducers([
  TypedReducer<HealthCheckState, LoadHealthCheckAction>(_loadHealthCheck),
  TypedReducer<HealthCheckState, HealthCheckSuccessAction>(_healthCheckSuccess),
  TypedReducer<HealthCheckState, HealthCheckFailureAction>(_healthCheckFailure),
]);

HealthCheckState _loadHealthCheck(
  HealthCheckState state,
  LoadHealthCheckAction action,
) {
  return state.copyWith(
    isLoading: true,
    data: null,
    isError: false,
  );
}

HealthCheckState _healthCheckSuccess(
  HealthCheckState state,
  HealthCheckSuccessAction action,
) {
  return state.copyWith(
    isLoading: false,
    data: action.data,
    isError: false,
  );
}

HealthCheckState _healthCheckFailure(
  HealthCheckState state,
  HealthCheckFailureAction action,
) {
  return state.copyWith(
    isLoading: false,
    data: null,
    isError: true,
  );
}
@DFreds
Copy link
Author

DFreds commented Feb 7, 2019

I feel kind of silly for not noticing all the callbacks given by the store connector. Anyway, I solved it using the onDidChange callback. Here is the final code for the success case for those who are interested.

class HealthCheckContainer extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return StoreConnector<AppState, _ViewModel>(
      distinct: true,
      converter: _ViewModel.fromStore,
      onDidChange: (_ViewModel viewModel) {
        if (viewModel.healthCheckData != null) {
          if (!viewModel.healthCheckData.isPlatformUp) {
            showDialog(
                context: context,
                builder: (BuildContext context) {
                  return AlertDialog(
                    title: Text('App down'),
                    content: Text('Oh noes'),
                  );
                });
          }

          viewModel.onNavigateToLogin();
        }
      },
      builder: (BuildContext context, _ViewModel viewModel) {
        return CenterLoading();
      },
    );
  }
}

class _ViewModel {
  final HealthCheckData healthCheckData;
  final Function onNavigateToLogin;

  _ViewModel({
    @required this.healthCheckData,
    @required this.onNavigateToLogin,
  });

  static _ViewModel fromStore(Store<AppState> store) {
    return _ViewModel(
      healthCheckData: store.state.healthCheckState.data,
      onNavigateToLogin: () => store.dispatch(NavigateToAction(route: "/login", clearStack: true))
    );
  }
}

@DFreds DFreds closed this as completed Feb 7, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant