Search code examples
flutterdartflutter-providerflutter-stateriverpod

StateNotifierProvider not keeping state between app restarts


Using flutter_riverpod: ^0.12.4 and testing in the android emulator as well as on a physical device.

What am I doing wrong that the Sign In screen state value does not persist in the StateNotifierProvider after a restart of the app?

The accountSetupProvider's state defaults to the Intro Screen. After the Intro Screen's onPressed button is clicked the state is updated to the Sign In screen and it triggers correctly a rebuild to display the Sign In screen.

However, after a flutter hot restart or opening/closing the app, the Intro Screen, rather than the Sign In screen displays. Shouldn't the state, which now is set to the Sign In screen after clicking onPressed in the Intro Screen persist between restarts and cause the Intro Screen to be skipped and the Sign In screen to display?

As you can see below main.dart has an initial AppRoutes.root route. In app_router.dart, this "root" screen opens root_screen.dart, which is a ConsumerWidget that is watch(ing) my StateNotifierProvider called "accountSetupProvider" in account_setup_provider.dart.

main.dart

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();

  runApp(
    ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      initialRoute: AppRoutes.root,
      onGenerateRoute: (settings) => AppRouter.onGenerateRoute(settings),
    );
  }
}

app_router.dart

class AppRoutes {
  static const String root = RootScreen.id;
  static const String intro = IntroScreen.id;
  static const String signIn = SignInScreen.id;
}

class AppRouter {
  static Route<dynamic> onGenerateRoute(RouteSettings settings) {
    final _args = settings.arguments;

    switch (settings.name) {
      case AppRoutes.root:
        return MaterialPageRoute<dynamic>(
          builder: (_) => RootScreen(),
          settings: settings,
        );
      case AppRoutes.intro:
        return MaterialPageRoute<dynamic>(
          builder: (_) => IntroScreen(),
          settings: settings,
        );
      case AppRoutes.signIn:
        return MaterialPageRoute<dynamic>(
          builder: (_) => SignInScreen(),
          settings: settings,
        );
    }
  }
}

root_screen.dart

class RootScreen extends ConsumerWidget {
  const RootScreen({Key key}) : super(key: key);
  static const String id = 'root_screen';

  @override
  Widget build(BuildContext context, ScopedReader watch) {
    final screen = watch(accountSetupProvider.state);

    if (screen == AppRoutes.signIn) {
      return SignInScreen();
    } else if (screen == AppRoutes.intro) {
      return IntroScreen();
    }
  }
}

intro_screen.dart (only including the onPressed portion of intro screen, which I'm expecting to set the state to the new screen, even after a flutter hot restart or app restart.)

onPressed: () {
  context
    .read(accountSetupProvider) // see accountSetupProvider StateNotifierProvider below.
    .setScreen(AppRoutes.signIn);
},

account_setup_provider.dart (inits to the AppRoutes.intro screen.)

class AccountSetupNotifier extends StateNotifier<String> {
  AccountSetupNotifier() : super(AppRoutes.intro);

  void setScreen(String screen) {
    state = screen;
  }
}

final accountSetupProvider = StateNotifierProvider<AccountSetupNotifier>((ref) {
  return AccountSetupNotifier();
});

Solution

  • Without even looking at your code, and looking only at your subject line, it is not at all surprising to me. "Hot Restart" resets all variables. How could there be any state preserved? Are you instead looking for "Hot Reload"?