skip to content

In a Flutter app, where are the launcher icon and native splash screen defined, and why can't a Flutter widget replace the native splash?

level: middleimportance: should knowfreq 38%

answer

  1. platform folders, not pubspec
  2. mipmap folders and Assets.xcassets
  3. LaunchTheme, NormalTheme, storyboard
  4. held until the first frame
  5. deferFirstFrame and allowFirstFrame

basics

~10 s

Both are native resources: Android icons in res/mipmap folders, iOS icons in Runner's Assets.xcassets; the splash is Android's LaunchTheme background and iOS's LaunchScreen.storyboard. They appear before Dart runs, so no widget can draw them.

solid answer

~40 s

The launcher icon is not a Flutter asset. On Android it lives in `android/app/src/main/res/mipmap-*` folders and is referenced by `android:icon="@mipmap/ic_launcher"` in `AndroidManifest.xml`; on iOS it is the app icon set in `Runner/Assets.xcassets`. Generators such as `flutter_launcher_icons` write those files from one source image. The native splash is also platform code: Android's `LaunchTheme` sets `windowBackground` to a drawable (with Android 12's SplashScreen API items for newer devices) and a `NormalTheme` named in `io.flutter.embedding.android.NormalTheme` metadata; iOS uses `LaunchScreen.storyboard`, which the App Store requires. They show while the OS starts the process and Flutter starts the engine — before any Dart runs — so a widget cannot replace them. Flutter keeps the launch screen up until its first frame; awaiting setup before `runApp`, or `deferFirstFrame()` and `allowFirstFrame()`, extends it.

code

xml · 12 lines
xml
<activity
    android:name=".MainActivity"
    android:theme="@style/LaunchTheme"
    android:exported="true">
    <meta-data
        android:name="io.flutter.embedding.android.NormalTheme"
        android:resource="@style/NormalTheme" />
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

go deeper

for a junior

Know that launcher icons and the native splash live in the android/ and ios/ folders, not in pubspec.yaml.

for a middle

Explain the cold-start sequence, LaunchTheme versus NormalTheme, and why the splash stays until Flutter's first frame.

for a senior

Make the hand-over seamless: defer the first frame for essential setup only, and match the first Flutter frame to the splash.

for a principal

Balance startup time against polish, deciding what may block the first frame and how splash assets stay in sync with the brand across flavours.

## Two images the Flutter framework never draws A pet-clinic app's paw logo appears in two places before any Flutter code runs: on the home screen as the **launcher icon**, and full-screen while the app starts as the **splash** (called a launch screen on iOS). Both are owned by the operating system, so they are configured in the platform folders of a Flutter project, not in `pubspec.yaml`, and no `Widget` can draw them. ## Launcher icons | Platform | Where the files live | How they are referenced | |---|---|---| | Android | `android/app/src/main/res/mipmap-*` density folders | `android:icon="@mipmap/ic_launcher"` on the `application` tag in `AndroidManifest.xml` | | iOS | the app icon set in `ios/Runner/Assets.xcassets` | selected in the Xcode project | - A new Flutter project ships placeholder icons in both places. - Android icons should follow the **adaptive icon** guidelines, since launchers mask them into different shapes. - Apple's app-icon guidelines cover light, dark and tinted variants, which the docs recommend reviewing. - The docs point to the `flutter_launcher_icons` package, which generates every size and folder from one source image; the alternative is placing the files by hand. Whatever the route, verify on a device: launchers cache icons, so reinstalling is sometimes needed to see the change. ## Launcher icons on the other targets The same rule — platform files, not Flutter assets — holds everywhere the template generates a project: - **Web:** `web/icons/` holds `Icon-192.png`, `Icon-512.png` and maskable variants referenced by `web/manifest.json`, plus `web/favicon.png`. - **macOS:** an `AppIcon.appiconset` inside `macos/Runner/Assets.xcassets`. - **Windows:** `windows/runner/resources/app_icon.ico`. Flavoured builds (a staging build with an orange paw, say) need separate icon resources per flavour in each platform's own mechanism; icon generators can usually produce those too. ## The native splash on Android The project template defines two themes in `styles.xml`: - **`LaunchTheme`**, whose `android:windowBackground` is a drawable (`@drawable/launch_background`). The activity starts in this theme, so the drawable is visible from the first moment. - **`NormalTheme`**, named in the activity's `io.flutter.embedding.android.NormalTheme` meta-data. Flutter switches to it once it takes over; its background shows only briefly, during orientation changes and activity restoration, so it should be a solid colour close to the app's background. On Android 12 and later the **SplashScreen API** applies: items such as `android:windowSplashScreenBackground` and `android:windowSplashScreenAnimatedIcon` in the launch theme define the splash. Apps supporting older versions often keep separate resources for each. ## The native splash on iOS The template includes `LaunchScreen.storyboard`, which by default shows a blank `LaunchImage`. Replace the images in the `LaunchImage` set in `Runner/Assets.xcassets`, or edit the storyboard in Xcode. The App Store requires apps to provide a launch screen as a storyboard. ## Why a widget cannot replace it The sequence at a cold start is: 1. The OS creates the process and shows the native splash. 2. The Flutter engine starts and the Dart isolate runs `main`. 3. `runApp` builds widgets and produces the first frame. 4. The first frame reaches the screen and the native splash is removed. A "splash widget" can only exist from step 3, so at best it is a second screen shown after the native one. Since Flutter 2.5 the embedding keeps the Android launch screen up **until the first Flutter frame**; the older `provideSplashScreen` hook on `FlutterActivity` was deprecated at that point. ## Holding the splash while the app loads Because removal waits for the first frame, delaying that frame extends the native splash: - Work awaited in `main` **before** `runApp` keeps the splash up, since no frame exists yet. - Inside the widget tree, `WidgetsBinding.instance.deferFirstFrame()` stops frames from reaching the engine until a matching `allowFirstFrame()`. The framework's own `Localizations` widget uses this while loading localized resources. Keep that window short and design the first Flutter frame to match the splash — same background, logo in the same place — so the hand-over is invisible. On Android 12+, the docs show removing the system exit animation when the first Flutter frame deliberately mirrors the splash.

  • Why is a Flutter SplashPage widget shown after startup not a replacement for the native splash?
    It can only appear once the engine has started and produced a frame, so the native splash has already been shown and removed. It becomes a second screen, often causing a visible jump. Better: keep the native splash until the first real frame, and make that frame look like the splash.
  • What does WidgetsBinding.instance.deferFirstFrame() do, and what must follow it?
    It tells the framework to keep producing frames but not send them to the engine, so the native launch screen stays visible. Each call must be matched by exactly one `allowFirstFrame()`, ideally while the scheduler is idle; otherwise the app never shows its first frame. It has no effect once the first frame has been sent.

saying these in an interview costs you the question

  • The launcher icon is declared under assets: in pubspec.yaml.
  • A splash widget passed to runApp replaces the native splash screen.
  • Flutter removes the Android launch screen before drawing its first frame.
  • iOS apps can ship without any launch screen storyboard.
  • deferFirstFrame() can be called without a matching allowFirstFrame().