Cross-Platform Mobile Development

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.

Choosing the Development Framework

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.

Building and Packaging Applications

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.

Managing Platform-Specific Code and Resources

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.

Integrating Native Libraries and APIs

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:

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.

Debugging and Runtime Observations

Runtime debugging reveals subtle failures. Common issues include:

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:

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.

Deploying and Releasing

Deployment involves building, testing, and shipping to app stores. The process includes:

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.

Summary of Key Patterns and Failures

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.