In Flutter, how do you read a saved language and an onboarding flag from shared_preferences before the first frame, avoiding a flash of the wrong screen?
answer
- plugins need the binding first
- await before runApp
- create() loads only allow-listed keys
- pass the instance down
- FutureBuilder shows a default first
basics
~10 sIn main, call WidgetsFlutterBinding.ensureInitialized(), await SharedPreferencesWithCache.create with an allowList of the two keys, then pass the instance into runApp so the first build reads locale and onboarding state synchronously.
solid answer
~40 sPlugins talk to the platform over channels, so before `runApp` you must call `WidgetsFlutterBinding.ensureInitialized()` or the first plugin call fails with "Binding has not yet been initialized". Then I `await SharedPreferencesWithCache.create(...)` with an `allowList` of just `languageCode` and `onboardingDone` — it loads only those keys — and pass the instance into the app. The first `build` reads both synchronously, so `MaterialApp` gets the right `locale` and the right home screen on frame one. Loading inside a `FutureBuilder` instead renders a default first and then swaps, which is the flash users notice. The native splash stays up while `main` awaits, so the read must stay small.
code
dart · 30 linesimport 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final prefs = await SharedPreferencesWithCache.create(
cacheOptions: const SharedPreferencesWithCacheOptions(
allowList: <String>{'languageCode', 'onboardingDone'},
),
);
runApp(App(prefs: prefs));
}
class App extends StatelessWidget {
const App({super.key, required this.prefs});
final SharedPreferencesWithCache prefs;
@override
Widget build(BuildContext context) {
final code = prefs.getString('languageCode');
final onboarded = prefs.getBool('onboardingDone') ?? false;
return MaterialApp(
locale: code == null ? null : Locale(code),
home: Scaffold(
body: Center(child: Text(onboarded ? 'Home' : 'Welcome tour')),
),
);
}
}go deeper
Recall that main must call WidgetsFlutterBinding.ensureInitialized() before using a plugin, and that preferences can be loaded before runApp.
Explain why awaiting a small allow-listed load in main avoids the flash that FutureBuilder or initState reads cause, and how the values reach the root widget.
Keep start-up reads minimal, measure time to first frame, and make one source of truth for onboarding state across routing and UI.
Set a start-up budget for the team: what may be awaited before the first frame, and what must load lazily afterwards.
## The problem Two settings decide what the very first screen looks like: the language the user chose and whether they finished onboarding. If the app draws before it knows them, the user sees English for a moment and then Portuguese, or glimpses onboarding before being bounced to the home screen. Getting both values before the first frame avoids that. ## Step 1: initialise the binding `shared_preferences` is a plugin; its calls cross a **platform channel**, which needs the services binding. `runApp` normally creates the binding, but code that runs before `runApp` has to create it itself: ```dart WidgetsFlutterBinding.ensureInitialized(); ``` Skip it and the first platform call throws a `FlutterError` whose summary reads "Binding has not yet been initialized." This is one of the most common start-up errors in Flutter projects that load preferences in `main`. ## Step 2: load only what the first frame needs `SharedPreferencesWithCache.create` is asynchronous: it fetches the allow-listed keys from the platform once, and afterwards every getter is synchronous. With an explicit `allowList`, that initial fetch reads only those keys rather than everything in the store. - `allowList: {'languageCode', 'onboardingDone'}` — two small values, a fast start. - `allowList: null` — every key the store holds is loaded, which grows as the app ages. `SharedPreferencesAsync` works too — you would `await` both getters in `main` — but the cached API lets widgets read the same values synchronously later, for example in a settings screen. ## Step 3: hand the values to the widget tree Pass the instance (or the two values) into the root widget through its constructor, or through whatever dependency-injection mechanism the app uses. The root `build` then decides synchronously: - `MaterialApp.locale` from `languageCode` (with `null` meaning follow the device); - the `home` — or the router's initial location — from `onboardingDone`. When the user later changes language, write it with `setString` and trigger a rebuild of the root through the app's state mechanism; the cache is already updated, so a synchronous read returns the new value. ## What the alternatives cost | Approach | First frame | Drawback | |---|---|---| | Await in `main`, then `runApp` | correct locale and screen | native splash stays up for the read | | `FutureBuilder` in the root widget | a placeholder or default | a visible flash or an extra loading screen | | Read in `initState` and `setState` | defaults | a rebuild after the first frame, same flash | | Hard-code defaults, fix later | wrong for returning users | returning users see the wrong screen | Awaiting in `main` delays the first Flutter frame, and the platform's launch screen stays visible meanwhile. For two keys that is a short wait; the lesson is to keep this path small, not to load the app's whole state before `runApp`. ## Common mistakes 1. Forgetting `ensureInitialized()` and getting the binding error. 2. Calling `SharedPreferences.getInstance()` inside `build`, creating a `Future` on every rebuild. 3. Caching everything with a `null` allowList and slowing every launch. 4. Using the onboarding flag to gate navigation in one place but reading it again, differently, in another, so the two disagree. ## In practice For the language-and-onboarding app, `main` initialises the binding, creates the cached preferences with a two-key allowList, and passes them to the root widget. The first frame shows the right language and the right screen, and nothing depends on a second read racing the first.
- Why does calling SharedPreferences before runApp fail without ensureInitialized?Plugin calls go over platform channels, which need the services binding. `runApp` creates the binding, but code in `main` before it runs too early, so the first channel call throws "Binding has not yet been initialized". `WidgetsFlutterBinding.ensureInitialized()` creates it up front.
- Why not simply use a FutureBuilder around MaterialApp?The first frame renders before the future completes, so you must show something: a spinner, which adds a screen, or defaults, which flash when the real values arrive. Awaiting in `main` keeps the native splash up instead and makes frame one correct.
saying these in an interview costs you the question
- Plugins can be called in main before runApp with no extra setup.
- Calling SharedPreferences.getInstance() inside build is fine because it is cached.
- Awaiting in main blanks the screen, so a FutureBuilder is always better.
- An allowList of null makes create() faster because nothing is filtered.
- SharedPreferencesWithCache getters must be awaited.