Skip to content
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

Asynchronously restoring state when initializing #82

Closed
emaddoma opened this issue Sep 26, 2018 · 3 comments
Closed

Asynchronously restoring state when initializing #82

emaddoma opened this issue Sep 26, 2018 · 3 comments

Comments

@emaddoma
Copy link

I'm pretty new to Flutter/Dart, so please bear with me as I try to explain what I'm trying to do...

My app state needs to be initialized with an object that is restored from shared preferences, if it exists. Basically, when the app first runs the user must select a casino from a list on a landing page. Doing so sets the object on the state and saves the id of the casino to shared preferences. The next time the app starts, it should restore the last viewed casino from preferences to the app state and show the home screen.

Now, I've actually got this working but A) I'm not sure this is the best way to do it and B) a non-fatal error is being thrown as soon as the app starts and I don't understand why.

main.dart

void main() {
  runApp(ReduxApp());
}

class ReduxApp extends StatelessWidget {

  Future<Store> restoreState() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    int casinoid = prefs.getInt(AppKeys.casinoPreference.toString());
    Casino casino = allCasinos.firstWhere((o) => o.id == casinoid, orElse: () => null);
    return createStore(casino);
  }

  Store<AppState> createStore(Casino casino) {
    Store<AppState> store = Store<AppState>(
      appReducer,
      initialState: AppState.loading(casino),
      middleware: createStoreAppMiddleware(),
    );
    return store;
  }

  @override
  Widget build(BuildContext context) {
    return new FutureBuilder(
      future: restoreState(),
      builder: (BuildContext context, AsyncSnapshot<Store> snapshot) {
        return StoreProvider<AppState>(
          store: snapshot.data,
          child: MaterialApp(
              title: ReduxLocalizations().appTitle,
              theme: AppTheme.theme,
              localizationsDelegates: [
                AppLocalizationsDelegate(),
                ReduxLocalizationsDelegate(),
              ],
              routes: {
                AppRoutes.landing: (context) {
                  return StoreBuilder<AppState>(
                    onInit: (store) => store.dispatch(LoadContentAction()),
                    builder: (context, store) {
                      if(store.state.casino != null){
                        return HomeScreen();
                      }
                      return LandingScreen();
                    },
                  );
                },
                AppRoutes.home: (context) {
                  return HomeScreen();
                },
                AppRoutes.demo: (context) {
                  return DemoScreen();
                }
              }
          )
        );
      }
    );
  }

}

The error

flutter: ══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
flutter: The following assertion was thrown building FutureBuilder<Store<dynamic>>(dirty, state:
flutter: _FutureBuilderState<Store<dynamic>>#a6574):
flutter: 'package:flutter_redux/flutter_redux.dart': Failed assertion: line 19 pos 16: 'store != null': is
flutter: not true.
flutter:
flutter: Either the assertion indicates an error in the framework itself, or we should provide substantially
flutter: more information in this error message to help you determine and fix the underlying cause.
flutter: In either case, please report this assertion by filing a bug on GitHub:
flutter:   https://github.com/flutter/flutter/issues/new
flutter:
flutter: When the exception was thrown, this was the stack:
flutter: #2      new StoreProvider (package:flutter_redux/flutter_redux.dart:19:16)
flutter: #3      ReduxApp.build.<anonymous closure> (file:///Users/erikm/Documents/Projects/Evergreen/egc/lib/main.dart:45:16)
flutter: #4      _FutureBuilderState.build (package:flutter/src/widgets/async.dart)
flutter: #5      StatefulElement.build (package:flutter/src/widgets/framework.dart:3742:27)
flutter: #6      ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:3654:15)
flutter: #7      Element.rebuild (package:flutter/src/widgets/framework.dart:3507:5)
flutter: #8      ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:3634:5)
flutter: #9      StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:3781:11)
flutter: #10     ComponentElement.mount (package:flutter/src/widgets/framework.dart:3629:5)
flutter: #11     Element.inflateWidget (package:flutter/src/widgets/framework.dart:2919:14)
flutter: #12     Element.updateChild (package:flutter/src/widgets/framework.dart:2722:12)
flutter: #13     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:3665:16)
flutter: #14     Element.rebuild (package:flutter/src/widgets/framework.dart:3507:5)
flutter: #15     ComponentElement._firstBuild (package:flutter/src/widgets/framework.dart:3634:5)
flutter: #16     ComponentElement.mount (package:flutter/src/widgets/framework.dart:3629:5)
flutter: #17     Element.inflateWidget (package:flutter/src/widgets/framework.dart:2919:14)
flutter: #18     Element.updateChild (package:flutter/src/widgets/framework.dart:2722:12)
flutter: #19     RenderObjectToWidgetElement._rebuild (package:flutter/src/widgets/binding.dart:881:16)
flutter: #20     RenderObjectToWidgetElement.mount (package:flutter/src/widgets/binding.dart:852:5)
flutter: #21     RenderObjectToWidgetAdapter.attachToRenderTree.<anonymous closure> (package:flutter/src/widgets/binding.dart:798:17)
flutter: #22     BuildOwner.buildScope (package:flutter/src/widgets/framework.dart:2235:19)
flutter: #23     RenderObjectToWidgetAdapter.attachToRenderTree (package:flutter/src/widgets/binding.dart:797:13)
flutter: #24     _WidgetsFlutterBinding&BindingBase&GestureBinding&ServicesBinding&SchedulerBinding&PaintingBinding&SemanticsBinding&RendererBinding&WidgetsBinding.attachRootWidget (package:flutter/src/widgets/binding.dart:689:7)
flutter: #25     runApp (package:flutter/src/widgets/binding.dart:728:7)
flutter: #26     main (file:///Users/erikm/Documents/Projects/Evergreen/egc/lib/main.dart:19:3)
flutter: #27     _startIsolate.<anonymous closure> (dart:isolate/runtime/libisolate_patch.dart:289:19)
flutter: #28     _RawReceivePortImpl._handleMessage (dart:isolate/runtime/libisolate_patch.dart:171:12)
flutter: (elided 2 frames from class _AssertionError)
flutter: ════════════════════════════════════════════════════════════════════════════════════════════════════
@brianegan
Copy link
Owner

brianegan commented Sep 28, 2018

Hey there!

This is definitely one way to do it, but perhaps not ideal.

Let's start with the error. The problem is you're using FutureBuilder, which will actually run the builder function twice: First, when snapshot.data is null. Then, when the sharedPrefs have been loaded, it will rebuild with the Store. Therefore, you're getting an error on the first build, because flutter_redux expects store is not null.

Overall, if you're going "The Redux Way," I'd recommend putting this "Loading" logic into your Redux flow!

  1. Add a loading boolean to your app state. If it's loading, show a spinner. If not, show the data.
  2. Create a "FetchInitialState" action, dispatch this when the app starts.
  3. Create a Middleware that listens for a "FetchInitialState" and loads the Data
  4. When it's loaded, dispatch a "InitialStateLoaded" action which is handled by your Reducer. It should set loading to false.

Now all of this logic is in the Store! You can now use your normal StoreConnector Widgets to react to these changes.

Hope this helps :) If not, let me know!

Btw, you can see this type of thing in the Flutter Architecture Samples redux app: https://github.com/brianegan/flutter_architecture_samples/blob/master/example/redux/lib/middleware/store_todos_middleware.dart#L45

@emaddoma
Copy link
Author

emaddoma commented Oct 3, 2018

Thanks!

@emaddoma emaddoma closed this as completed Oct 3, 2018
@sanjaycedti
Copy link

Hi @brianegan ,
In my case, I have multiple actions to dispatch in the middleware but having a hard time to determine how to dispatch the action SyncLocalDataCompleteAction when all the actions are completed?

The dispatch method always returns null.

Epics

Stream fetchUserEpic(Stream actions, EpicStore<AppState> store) =>
    actions.whereType<FetchLocalUserRequestAction>()
    .debounceTime(Duration(seconds: 5))
    .switchMap((action) => _fetchUser());

  Stream _fetchUser() => 
    handleException<AuthResponse, FetchLocalUserResultAction, FetchLocalUserErrorAction>(
      api: () => _mergeUser(),
      onResult: (result) => FetchLocalUserResultAction(result),
      onError: (error) => FetchLocalUserErrorAction(error)
    );

  Future<AuthResponse> _mergeUser() async {
      final token = await SharedPreferenceService.getAccessToken();
      final user = await SharedPreferenceService.getUser();

      return AuthResponse(token: token, user: user);
  }

  Stream fetchPreferredLanguageEpic(Stream actions, EpicStore<AppState> store) =>
    actions.whereType<FetchPreferredLanguageRequestAction>()
    .switchMap((action) => _fetchPreferredLanguage());

  Stream _fetchPreferredLanguage() =>
    handleException<String, FetchPreferredLanguageResultAction, FetchPreferredLanguageErrorAction>(
      api: () => SharedPreferenceService.getLanguageCode(),
      onResult: (code) => FetchPreferredLanguageResultAction(code),
      onError: (error) => FetchPreferredLanguageErrorAction(error)
    );

   Stream fetchOnboardingStatusEpic(Stream actions, EpicStore<AppState> store) =>
    actions.whereType<FetchOnboardingStatusRequestAction>()
    .switchMap((action) => _fetchOnboardingStatus());

  Stream _fetchOnboardingStatus() =>
    handleException<OnboardingStatus, FetchOnboardingStatusResultAction, FetchOnboardingStatusErrorAction>(
      api: () => SharedPreferenceService.getOnboardingStatus(),
      onResult: (status) => FetchOnboardingStatusResultAction(status),
      onError: (error) => FetchOnboardingStatusErrorAction(error)
    );

Middleware

syncLocalData<State>(
  Store<State> store,
  action,
  NextDispatcher next
){
  if(action is SyncLocalDataRequestAction){
    final xx = store.dispatch(FetchPreferredLanguageRequestAction());
    print('xx: $xx');
    store.dispatch(FetchLocalUserRequestAction());
    store.dispatch(FetchOnboardingStatusRequestAction());
  }

  next(action);
}

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

3 participants