If you've ever had to ship a white-label app, a staging build alongside a production build, or simply a "dev" flavour that points to a different API endpoint, you already know the pain of manually swapping icons and splash screens before every release. App variants solve this problem cleanly. In my experience building client apps for regulated industries, getting variants right early saves dozens of hours across a project's lifetime.
What Are App Variants?
An app variant is a distinct build of the same codebase that can have a different bundle ID, app name, icon, splash screen, and environment configuration. On Android these are called product flavors; on iOS they are managed through schemes and targets. A single React Native repo can produce a dev, staging, and production variant — all installable side-by-side on the same device.
Android: Product Flavors in build.gradle
Open android/app/build.gradle and add a flavorDimensions block inside android {}:
android {
flavorDimensions "env"
productFlavors {
dev {
dimension "env"
applicationIdSuffix ".dev"
versionNameSuffix "-dev"
resValue "string", "app_name", "MyApp Dev"
}
staging {
dimension "env"
applicationIdSuffix ".staging"
versionNameSuffix "-staging"
resValue "string", "app_name", "MyApp Staging"
}
production {
dimension "env"
resValue "string", "app_name", "MyApp"
}
}
}
Each flavor gets its own source set at android/app/src/<flavorName>/. Assets placed there override the main source set, which is where variant-specific icons and splash screens live.
Android Splash Screen Per Variant
React Native 0.73+ uses the native SplashScreen API. Place a variant-specific launch_screen.xml inside android/app/src/dev/res/drawable/. The build system automatically picks the correct drawable for each flavor.
android/app/src/
├── dev/
│ └── res/
│ ├── drawable/
│ │ └── launch_screen.xml ← dev splash (orange brand)
│ └── mipmap-*/
│ └── ic_launcher.png ← dev icon
├── staging/
│ └── res/
│ ├── drawable/
│ │ └── launch_screen.xml ← staging splash (yellow brand)
│ └── mipmap-*/
│ └── ic_launcher.png ← staging icon
└── main/
└── res/
├── drawable/
│ └── launch_screen.xml ← production splash
└── mipmap-*/
└── ic_launcher.png ← production icon
App Icon Management on Android
Generate icons at all required densities (mdpi, hdpi, xhdpi, xxhdpi, xxxhdpi) using Android Asset Studio or the Figma Android Icon exporter. Drop the resulting mipmap-* folders into the matching flavor source set. The production icon stays in main/res/ and serves as the fallback.
iOS: Schemes and Targets
In Xcode, duplicate your existing target for each environment. Name them MyApp-Dev, MyApp-Staging, and MyApp (production). For each duplicated target:
- Set a unique Bundle Identifier (e.g.,
com.mycompany.myapp.dev). - Create a matching scheme so you can build from the command line.
- Assign a separate Asset Catalog or use a single catalog with multiple AppIcon sets, one per target.
# Build the dev scheme from the CLI
xcodebuild -workspace ios/MyApp.xcworkspace -scheme MyApp-Dev -configuration Debug -sdk iphonesimulator
iOS Splash Screen Per Variant
React Native uses LaunchScreen.storyboard by default. Create a copy — LaunchScreen-Dev.storyboard — and reference it in your MyApp-Dev target's Info.plist under the UILaunchStoryboardName key. This lets each target render a completely different splash layout and color without touching shared code.
Reading the Active Flavor in JavaScript
Use react-native-config to expose environment variables to JS based on the active variant:
// .env.dev
API_URL=https://api-dev.myapp.com
APP_ENV=development
// .env.production
API_URL=https://api.myapp.com
APP_ENV=production
import Config from 'react-native-config';
console.log(Config.API_URL); // "https://api-dev.myapp.com" in dev build
console.log(Config.APP_ENV); // "development"
Build Commands
# Android — run the dev flavor on a connected device
npx react-native run-android --variant=devDebug
# Android — assemble a release APK for staging
cd android && ./gradlew assembleStagingRelease
# iOS — run the Dev scheme in the simulator
npx react-native run-ios --scheme MyApp-Dev
Debugging Tips
- Icons not updating? Run
./gradlew cleanon Android or delete the DerivedData folder on iOS. - Wrong splash on iOS? Confirm
UILaunchStoryboardNameis set per-target, not just in the shared Info.plist. - Bundle ID collision? Each flavor must produce a unique applicationId, otherwise the OS treats them as the same app and the install will overwrite the other variant.
Setting up variants properly takes about half a day up front, but the payoff — clean, one-command builds for every environment — is worth every minute. If you're managing multiple white-label clients from one codebase, this pattern is non-negotiable.

