Google Play Closed Testing for Flutter and React Native Apps
Cross-platform release builds behave differently from the debug build in your editor. What to check before a Flutter or React Native app enters closed testing.
The build testers install is not the build you develop against
Everything you have run until now has almost certainly been a debug build. It was signed with a debug key, minification was off, and several optimisations that only engage in release never ran. Closed testing installs a release build from the Play Store instead, and that is a different program.
The practical consequence: a bug that exists only in release is invisible to you until it is in front of testers. Building release is one command in either framework.
- Flutter:
flutter build appbundle --release - React Native:
cd android && ./gradlew bundleRelease
Minification is where cross-platform plugins break
R8 shrinking and resource shrinking are on by default in release builds. They remove code your app never references directly, which is exactly the code that reflection depends on.
Several libraries you inherited without thinking about use reflection: JSON serialisers, networking interfaces, and some analytics or attribution SDKs. Flutter plugins with platform channels carry the same exposure, because the channel bridges reference classes by name at runtime. When R8 renames or removes those classes, the plugin stops working. Usually it fails loudly. Occasionally it fails silently.
This is the single most common reason a cross-platform app that works perfectly in the editor falls over on the closed track. Keep rules fix it, and both frameworks let published plugins ship them automatically. Read what your plugins declare rather than assuming.
You are uploading one artifact, not the file testers get
Play requires an Android App Bundle for new apps and updates. You upload one .aab, and Play generates per-device split APKs from it. So the artifact on your disk is not the artifact installed on a tester's phone, and sideloading your own bundle will not reproduce what they see.
That matters if you want to test the real install path. For direct installation, you need APKs, and the two frameworks split them differently.
- Flutter:
flutter build apk --release --split-per-abiproduces one APK per ABI. Drop the flag for a single larger universal APK. - React Native: splits are configured through
splits.abiin the Android Gradle config, not by a CLI flag.
Either way, the route testers actually use is the Play Store, so the version you smoke test should be the one delivered through the track.
Version codes, or why a tester reports a bug you already fixed
Play will not deliver an update whose versionCode is the same as or lower than what a device already has. Build again without incrementing it and testers quietly keep the old build. The symptom is a report about a bug you fixed two days ago, and it wastes a day of everyone's attention.
Increment it as part of the build rather than by hand. Flutter takes --build-number on the command line. React Native reads versionCode from the Gradle config, which is easy to wire to a CI build number.
- Confirm the version code in Play Console matches the build you just produced.
- Tell testers to update, because Play does not always push it silently.
- Keep the build you shipped until the window closes, so a report can be tied to a specific release.
What emulators never show you
Emulators are fast and wrong in ways that matter once real devices are involved. OEM skins, keyboard behaviour, permission dialogs, notification handling and especially WebView versions vary by device in ways no emulator image reproduces.
The two frameworks differ here, and it is worth knowing which risk you carry. Flutter draws its own interface, so visual differences across devices are fewer, but text scaling, font fallback and platform channel behaviour still vary. React Native renders through native views, so OEM theming and WebView version affect it directly.
Play Console lists the device catalogue your app supports. Read it before choosing a tester roster, because a roster of twelve identical phones proves less than it looks like it does.
A pre-flight check before the window opens
- Build release, not debug, and install it on a physical device yourself.
- Increment the version code as part of the build.
- Confirm the release signing config uses the upload key Play expects.
- Check that reflection-based plugins and SDKs have working keep rules.
- Smoke test launch, login and your main flow on a real device.
- Verify the version code shown in Play Console matches the build you produced.
- Only then start the continuous testing window.
A day spent on this is cheaper than discovering a minification crash on launch. A crash at launch is worse than a bug, because participation is what the requirement measures, and an app that will not open burns the streak you need.
Where the tester requirement fits in
The tester count is measured against accounts that are opted in, not invitations sent. Google also expects a continuous period of participation rather than a total, so a crash that empties the roster for two days costs you more than two days.
The current figures and eligibility rules live in App testing requirements for new personal developer accounts. Google sets them and can change them, so confirm the position for your own account type before planning a window.
If you are setting up the track itself, Set up an open, closed, or internal test covers the console side.
The trade-off, stated plainly
Ready to Start Your Closed Test?
Register, create the app and choose your tester count. Every plan includes the same monitoring, support and participation guarantee.