Choose and implement the right cross-platform mobile framework. Covers React Native, Flutter, Kotlin Multiplatform, architecture differences, performance implications, native bridge patterns, and the decision framework for platform selection.
Cross-platform mobile development enables a single codebase to target both iOS and Android, reducing duplication and accelerating iteration. It matters when teams need to ship features across platforms in parallel, maintain consistent user experiences, and scale development velocity without doubling headcount. This approach is most effective when UI fidelity, shared business logic, and rapid release cycles are prioritized over platform-specific nuances.
The core decision is selecting a framework that compiles or runs code across platforms. The framework must support declarative UI, state management, and native access. Key considerations include:
Common patterns in framework selection include:
To evaluate a framework, run the following command in a project directory:
npm run build -- --platform=ios --mode=production --optimize
This command compiles the application for iOS with tree-shaking, minification, and code splitting. It expects a webpack.config.js with:
module.exports = {
entry: './src/index.ts',
output: {
path: './build/ios',
filename: 'main.js',
libraryTarget: 'umd',
},
resolve: {
extensions: ['.ts', '.js', '.tsx'],
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
inject: 'body',
}),
],
};
Failures often occur when the build process omits platform-specific assets. For example, missing AppIcon.appiconset in ios/ or android/app/src/main/res/mipmap-hdpi/ic_launcher.png results in blank icons on launch.
To produce a distributable app, run a build command that compiles assets, bundles the code, and packages the result. The output should be a .ipa (iOS) or .apk (Android) file.
For Android, the ./gradlew assembleRelease command generates an APK in app/build/outputs/apk/release/app-release.apk. It uses build.gradle with:
android {
compileSdkVersion 34
defaultConfig {
applicationId "com.example.app"
minSdkVersion 21
targetSdkVersion 34
versionCode 100
versionName "1.2.0"
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
The proguard-rules.pro file must preserve public classes and methods used by native code:
-keep public class com.example.app.MainActivity {
public <init>(...);
public void onCreate(android.os.Bundle);
}
-keep class com.example.app.api.* { *; }
Failure to include these rules results in ClassNotFoundException on startup, particularly when calling into native code via JNI.
For iOS, xcodebuild -scheme MyApp -configuration Release -archivePath build/MyApp.xcarchive archive creates a .xcarchive bundle. To export a .ipa:
xcodebuild -exportArchive -archivePath build/MyApp.xcarchive \
-exportPath build/MyApp.ipa \
-exportOptionsPlist exportOptions.plist
The exportOptions.plist must define:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store</string>
<key>provisioningProfiles</key>
<dict>
<key>com.example.app</key>
<string>MyApp Distribution</string>
</dict>
<key>compileBitcode</key>
<true/>
</dict>
</plist>
Missing keys like method or incorrect provisioning profile names cause the export to fail silently: the .ipa is created but fails to install on devices due to mismatched bundle identifiers or missing entitlements.
Platform-specific code is split into ios/ and android/ directories. Each platform may define its own main.dart, MainActivity.java, or AppDelegate.swift. The framework must resolve platform-specific assets and code at runtime.
A common failure is missing resource resolution. For example, in a Flutter app, the pubspec.yaml must list all assets:
flutter:
assets:
- assets/images/
- assets/config.json
- assets/languages/en.json
- assets/fonts/Roboto-Regular.ttf
If assets/images/logo.png is not listed, Image.asset('logo.png') returns null. The error is silent because the asset is in the assets directory but not bundled into the app binary.
To resolve platform-specific code, use conditional compilation. In Dart:
import 'package:flutter/foundation.dart' as foundation;
void main() {
if (foundation.kIsWeb) {
runApp(WebApp());
} else if (foundation.kIsLinux) {
runApp(LinuxApp());
} else if (foundation.kIsIOS) {
runApp(IOSApp());
} else if (foundation.kIsAndroid) {
runApp(AndroidApp());
}
}
The kIsIOS and kIsAndroid constants are defined at compile time. If the main.dart is not compiled into the platform-specific binary, the conditionals evaluate to false and the app runs on the wrong platform.
To enable platform-specific code in android/, create android/app/src/main/java/com/example/app/MainActivity.java with:
package com.example.app;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Platform-specific setup
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
getWindow().setStatusBarColor(Color.parseColor("#1A5F9A"));
}
}
}
The Build.VERSION_CODES.M constant must be imported. Failure to import android.os.Build results in cannot find symbol errors at compile time.
To access device-specific features (camera, GPS, BLE, push notifications), the framework must provide a native bridge. The bridge enables calling platform-specific APIs from the shared codebase.
Common patterns include:
To register a method channel in Android:
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugin.common.MethodChannel;
public class MainActivity extends FlutterActivity {
private static final String CHANNEL = "com.example.app/camera";
@Override
public void configureFlutterEngine(FlutterEngine flutterEngine) {
super.configureFlutterEngine(flutterEngine);
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), CHANNEL)
.setMethodCallHandler((call, result) -> {
if (call.method.equals("takePhoto")) {
takePhoto(result);
} else {
result.notImplemented();
}
});
}
private void takePhoto(MethodChannel.Result result) {
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (intent.resolveActivity(getPackageManager()) != null) {
startActivityForResult(intent, 1001);
result.success("Camera started");
} else {
result.error("CAMERA_NOT_AVAILABLE", "No camera app found", null);
}
}
}
In Dart, call the native method:
final MethodChannel channel = MethodChannel('com.example.app/camera');
Future<void> takePhoto() async {
try {
final String result = await channel.invokeMethod('takePhoto');
print('Photo taken: $result');
} on PlatformException catch (e) {
print('Failed to take photo: ${e.message}');
}
}
The PlatformException is thrown when the native code fails to return a value or throws an uncaught exception. Common causes:
android:exported="true" in AndroidManifest.xml.Intent not handled by any app.Activity not registered in manifest.To enable the method channel, the AndroidManifest.xml must declare the MainActivity:
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|screenLayout|locale|uiMode"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
Failure to set android:exported="true" causes ActivityNotFoundException when launching the app from the home screen.
Runtime debugging reveals subtle failures. Common issues include:
images/ directory).MainActivity.java not compiled).To debug a Flutter app on Android, use flutter logs:
flutter logs --device-id=1234567890 --follow
The output shows logs from both Dart and native code. Key artifacts include:
I/flutter ( 1234): — Dart-level logs.V/MainActivity( 1234): — Android native logs.E/MethodChannel( 1234): — Method channel errors.A common error is:
E/MethodChannel( 1234): PlatformException(camera_error, Failed to open camera, null, null)
E/MethodChannel( 1234): at io.flutter.embedding.engine.FlutterEngine.handlePlatformMessage(FlutterEngine.java:263)
E/MethodChannel( 1234): at io.flutter.embedding.engine.dart.DartMessenger.send(DartMessenger.java:123)
E/MethodChannel( 1234): at io.flutter.plugin.common.MethodChannel.invokeMethod(MethodChannel.java:145)
This indicates that the takePhoto method failed on the Android side but did not propagate the error back to Dart.
To capture and analyze performance, use flutter profile:
flutter profile --trace-file=profile.json --timeline
The resulting profile.json can be opened in Chrome’s about:tracing to inspect frame rendering, garbage collection, and method execution.
Deployment involves building, testing, and shipping to app stores. The process includes:
.p12 (iOS) and .keystore (Android) files.versionCode (Android) and CFBundleShortVersionString (iOS).For iOS, fastlane is commonly used:
lane :release do
increment_version_number
build_app(scheme: "MyApp")
upload_to_testflight
promote_to_production
end
The Fastfile must define increment_version_number correctly:
lane :increment_version_number do
increment_version_number(
version_number: "1.2.1"
)
end
Failure to set CFBundleShortVersionString in Info.plist causes the app to appear as “1.0” in TestFlight, even after multiple releases.
For Android, gradle is used:
./gradlew bundleRelease --stacktrace
The --stacktrace flag reveals Gradle-specific errors, such as:
> Task :app:compileDebugKotlin
e: /app/src/main/java/com/example/app/AnalyticsService.kt:24:52: Unresolved reference: analytics
This error points to a missing import or a misspelled class name.
MainActivity.java not included in APK.android:exported="true".versionCode and CFBundleShortVersionString diverge..p12 and .keystore files must be correctly configured and versioned.These patterns define effective cross-platform mobile development: a disciplined, repeatable process that minimizes surprises and maximizes consistency across platforms.
This page was rewritten on 17 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy