Flutter App Store Screenshots: integration_test
integration_test +
flutter drive, render widget snapshots with golden tests (great for regression,
a stretch for store assets), or wrap either in Fastlane/Codemagic CI. None of those compose the
marketing carousel — captions, frames, backgrounds, 50-locale copy. That is a separate step, and
it is where Mokbi fits: it composes and localizes the screenshots you captured. It does
not capture them.
If you build with Flutter and you have shipped to the App Store or Play Store, you have already
hit the gap: there is no flutter screenshots. Apple wants exact-pixel PNGs at sizes
like 1320 × 2868 (6.9-inch iPhone) and 2064 × 2752 (13-inch iPad); Flutter hands you a rendering
engine and a test harness and leaves the store pipeline to you. This post is the honest map of
what actually works in 2026, with code you can paste, and a clear line about which tool does which
job. If you want the framework-specific overview first, see
Mokbi for Flutter developers.
One framing up front, because it saves a lot of confusion: capturing a frame (driving the app to a state and saving a PNG of what is on screen) and composing a store screenshot (that frame inside a device mockup, with a headline, a background, and translated copy, exported at every required dimension) are two different problems. Most of the Flutter tooling below solves the first. None of it solves the second.
Option 1: integration_test + flutter drive (real captured frames)
This is the closest Flutter has to an official screenshot path. The
integration_test package launches your full app on a real device or emulator, drives
it with the same finder/tester API as widget tests, and exposes
binding.takeScreenshot(). The catch is that takeScreenshot returns the
bytes to the test process — it does not write a file — so you need the
extended driver to receive those bytes on the host and save them.
First, the test. Note the Android-only dance: convertFlutterSurfaceToImage() must be
called (and a frame pumped) before capture, because Android renders Flutter into a
SurfaceView that the framebuffer can't read back directly.
// integration_test/screenshot_test.dart
import 'dart:io' show Platform;
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:my_app/main.dart' as app;
void main() {
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('home + detail screenshots', (tester) async {
app.main();
await tester.pumpAndSettle();
// Android needs the surface converted to an image before capture.
if (Platform.isAndroid) {
await binding.convertFlutterSurfaceToImage();
await tester.pumpAndSettle();
}
await binding.takeScreenshot('01_home');
await tester.tap(find.byKey(const Key('open-detail')));
await tester.pumpAndSettle();
await binding.takeScreenshot('02_detail');
});
}
Then the driver, which runs on the host machine and writes the PNG bytes the test sends back.
This file lives at test_driver/ and uses
integration_test_driver_extended.dart (the plain integration_test_driver.dart
has no onScreenshot hook):
// test_driver/integration_test.dart
import 'dart:io';
import 'package:integration_test/integration_test_driver_extended.dart';
Future<void> main() async {
await integrationDriver(
onScreenshot: (String name, List<int> bytes,
[Map<String, Object?>? args]) async {
final file = await File('screenshots/$name.png').create(recursive: true);
file.writeAsBytesSync(bytes);
return true; // false fails the test (e.g. on a diff mismatch)
},
);
}
Crucially you must run this with flutter drive, not flutter test —
only the driver path invokes onScreenshot:
flutter drive \
--driver=test_driver/integration_test.dart \
--target=integration_test/screenshot_test.dart \
-d emulator-5554 What you get and what you don't:
- Real app pixels. These are screenshots of your actual running widgets, not a re-render — fonts, theme, dynamic data, all native.
- Deterministic states. You drive the app to exactly the screens you want and capture on demand.
- Device class = the device you run on. The output resolution is whatever the emulator/device is. To hit Apple's exact 1320 × 2868 you run on a 6.9-inch-class iOS simulator; for the 13-inch iPad set you run that simulator. There is no magic "resize to App Store" flag.
- Locale is your job. You can pass a locale into your app at boot and re-run per language, but that captures the app UI in that locale — it does not write your marketing headline copy.
- Maintenance cost is real. The finders (
find.byKey, taps) break when the UI changes, same as any UI test.
Option 2: golden tests — and why they are not store assets
Flutter's golden tests (matchesGoldenFile, or the higher-level
testGoldens from packages like golden_toolkit) render a widget tree
to an image and compare it byte-for-byte against a committed reference. They are excellent, and
they are built for a different purpose than store screenshots: pixel regression.
The point is to catch the day a padding change or a font swap silently shifts your UI. The golden
is the contract; a diff is a failed test.
Two reasons not to reach for raw golden tests as your store pipeline:
- Goldens render widgets in a headless test environment, not the real device surface. Platform views, some shaders, and anything that depends on the actual GPU compositor can render differently (or not at all) versus what a user sees. For a regression contract that's fine — you compare against yourself. For a store asset you want the real frame, which is what Option 1 gives you.
- Pixel-perfect matching fights you. Vanilla goldens expect an exact match, so antialiasing and font-hinting differences across machines cause flaky failures. That is the right strictness for regression and the wrong strictness for "produce a nice marketing image."
That said, there is a legitimate middle path. Packages such as
golden_screenshot
deliberately repurpose the golden machinery for store output: they use testGoldens,
wrap your screen in real device frames (phone/tablet/desktop), enable a fuzzy comparator that
tolerates a small (~0.1%, configurable) mismatch instead of demanding pixel-perfection, and write
files into a Fastlane-style directory layout
(metadata/<locale>/images/...). If your screens are pure-Flutter (no platform
views) and you want frames-around-a-render without spinning up an emulator, this is a reasonable
and fast route. Just be clear-eyed that you are rendering widgets, not capturing the live app, and
that the "marketing" you get is a device frame — not headlines, backgrounds, or a multi-panel
carousel.
Option 3: Fastlane / Codemagic for Flutter
Fastlane is iOS/Android automation, not Flutter-specific, but it slots in two ways. Fastlane's own
capture tools (snapshot for iOS via XCUITest, screengrab for Android via
an Espresso/UI Automator harness) expect native UI tests, which Flutter apps generally don't have —
so most Flutter teams don't use snapshot to capture. Where Fastlane earns its
place is delivery: deliver (iOS) and supply (Android)
upload your already-captured PNGs and metadata to App Store Connect / Play Console, and they read a
Fastlane-shaped folder — which is exactly the layout golden_screenshot writes and the
layout you can have your Option 1 driver write too.
Codemagic
is the Flutter-native CI most teams reach for here. The realistic CI recipe is: run your
integration_test screenshot drive on the macOS/Android runners across the device
classes you need, collect the PNGs as build artifacts, then call deliver/supply
to push them. This is genuinely useful when screenshots drift on every release and you ship often.
For the broader "automate vs no-code" trade-off — including when this setup is overkill — see
Fastlane snapshot vs no-code App Store screenshots.
Option 4: manual capture (still completely valid)
For a lot of indie Flutter apps, the honest answer is: just take the screenshots by hand once per release. Run the app in the iOS Simulator at the right device class (6.9-inch iPhone, 13-inch iPad), navigate to each screen, and use the simulator's screenshot command — it captures at the exact device resolution, which is what App Store Connect wants. On Android, the emulator's camera button does the same. Total time for a five-screen app: maybe ten minutes.
Manual capture has no maintenance cost, no flaky finders, and no CI to babysit. Its only weakness is that nothing regenerates automatically — if you ship a UI change, you re-shoot. For a team releasing a few times a year, that trade is overwhelmingly worth it. The automation in Options 1–3 pays off precisely when "re-shoot by hand" stops being a ten-minute job because you ship weekly and in many locales.
The part none of the above does: composition + 50-language localization
Here is the bounded, honest paragraph. Every option above produces bare frames — your app
UI at a device resolution. App Store and Play Store carousels that convert are not bare frames:
they are the frame inside a device mockup, on a background, under a short headline, often as a
multi-panel sequence, and repeated across every locale you target. That composition step is what
Mokbi is for. You bring the frames you captured (from integration_test,
golden_screenshot, the simulator, wherever), drop them into a browser editor, add
device frames and captions and backgrounds, translate the captions across up to 50 App Store
languages in roughly one click, and batch-export at every required size. It is free to design with
a watermarked preview; export is a one-time purchase (Single €9.99 / Pro €29.99) with no
subscription. To be explicit about the boundary: Mokbi does not capture source
screenshots — it does not run your app or drive your widgets. Capture stays with the
Flutter tooling above; Mokbi composes and localizes what that tooling produces.
A realistic combined Flutter workflow
- Capture the source frames. Write an
integration_testscreenshot drive (Option 1) for the screens you want in the carousel, or — if you ship infrequently — just capture them manually from the simulator. Run on a 6.9-inch iPhone class device and a 13-inch iPad class device so the resolutions are right. - (Optional) Wire it into CI. If drift is a real problem, run the drive on Codemagic and collect the PNGs as artifacts (Option 3). If it isn't, skip this entirely.
- Compose the marketing carousel. Drop the frames into Mokbi, add frames/captions/backgrounds, build the multi-panel sequence. This is the step the Flutter tooling can't do.
- Localize and export. One-click translate captions across your target App Store locales, batch-export every required dimension, and (if you like) drop the output into a Fastlane folder so
deliver/supplyuploads it. - On the next UI change, re-capture and re-open. Re-run the drive (or re-shoot), re-open the saved Mokbi project, swap the frames, re-export. The composition and translations are preserved.
When to skip the automation entirely
If you ship one to three releases a year, building an integration_test screenshot
harness and a CI pipeline will cost you more hours than it ever saves. Capture manually from the
simulator, compose and localize once, and move on. The automation in Options 1–3 is for teams where
screenshot drift is a recurring tax — frequent releases, many locales, or several apps in a
portfolio. Match the machinery to the release cadence, not to what looks rigorous.
Before you export anything, sanity-check the targets: get the exact pixel dimensions from the App Store screenshot sizes guide and the format rules (PNG/JPEG, RGB, no alpha channel, 1–10 per device class) from the App Store screenshot requirements guide. Apple rejects off-by-one dimensions with no tolerance, so this is worth thirty seconds.