androidengineers.Book a session

Cross-Platform Considerations

Bridging Native Capabilities

article20 minHard

Cross-platform frameworks can't expose every native API. When you need camera access, Bluetooth, biometrics, or a platform-specific SDK, you write a bridge between the framework and native code.

React Native: Native Modules

// Android side: TurboModule (new architecture) or legacy NativeModule
@ReactModule(name = BiometricModule.NAME)
class BiometricModule(reactContext: ReactApplicationContext) :
    ReactContextBaseJavaModule(reactContext) {

    companion object { const val NAME = "BiometricModule" }

    override fun getName(): String = NAME

    @ReactMethod
    fun authenticate(reason: String, promise: Promise) {
        val activity = currentActivity ?: run {
            promise.reject("NO_ACTIVITY", "No current activity")
            return
        }

        val executor = ContextCompat.getMainExecutor(reactApplicationContext)
        val biometricPrompt = BiometricPrompt(activity as FragmentActivity, executor,
            object : BiometricPrompt.AuthenticationCallback() {
                override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                    promise.resolve("success")
                }
                override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                    promise.reject("AUTH_ERROR_$errorCode", errString.toString())
                }
                override fun onAuthenticationFailed() {
                    promise.reject("AUTH_FAILED", "Authentication failed")
                }
            })

        val promptInfo = BiometricPrompt.PromptInfo.Builder()
            .setTitle("Authenticate")
            .setSubtitle(reason)
            .setNegativeButtonText("Cancel")
            .build()

        biometricPrompt.authenticate(promptInfo)
    }
}

// Register in Package
class BiometricPackage : ReactPackage {
    override fun createNativeModules(context: ReactApplicationContext) =
        listOf(BiometricModule(context))
    override fun createViewManagers(context: ReactApplicationContext) = emptyList<ViewManager<*, *>>()
}
// JavaScript side
import { NativeModules } from 'react-native';
const { BiometricModule } = NativeModules;

async function authenticate() {
  try {
    const result = await BiometricModule.authenticate('Confirm your identity');
    console.log('Authenticated:', result);
  } catch (e) {
    console.error('Auth failed:', e.message);
  }
}

Flutter: Platform Channels

// Android side: MethodChannel handler
class MainActivity : FlutterActivity() {
    private val CHANNEL = "com.myapp/biometric"

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
            .setMethodCallHandler { call, result ->
                when (call.method) {
                    "authenticate" -> {
                        val reason = call.argument<String>("reason") ?: "Authenticate"
                        authenticate(reason, result)
                    }
                    else -> result.notImplemented()
                }
            }
    }

    private fun authenticate(reason: String, result: MethodChannel.Result) {
        val executor = ContextCompat.getMainExecutor(this)
        val prompt = BiometricPrompt(this, executor,
            object : BiometricPrompt.AuthenticationCallback() {
                override fun onAuthenticationSucceeded(r: BiometricPrompt.AuthenticationResult) {
                    result.success("success")
                }
                override fun onAuthenticationError(code: Int, msg: CharSequence) {
                    result.error("AUTH_ERROR", msg.toString(), null)
                }
                override fun onAuthenticationFailed() {
                    result.error("AUTH_FAILED", "Authentication failed", null)
                }
            })

        prompt.authenticate(BiometricPrompt.PromptInfo.Builder()
            .setTitle("Authenticate")
            .setSubtitle(reason)
            .setNegativeButtonText("Cancel")
            .build())
    }
}
// Flutter/Dart side
class BiometricService {
  static const _channel = MethodChannel('com.myapp/biometric');

  Future<bool> authenticate(String reason) async {
    try {
      final result = await _channel.invokeMethod<String>('authenticate', {'reason': reason});
      return result == 'success';
    } on PlatformException catch (e) {
      debugPrint('Biometric error: ${e.message}');
      return false;
    }
  }
}

KMM: expect/actual for Native SDKs

// commonMain: declare expected platform function
expect class PlatformBiometric {
    suspend fun authenticate(reason: String): Boolean
}

// androidMain: actual implementation
actual class PlatformBiometric(private val activity: FragmentActivity) {
    actual suspend fun authenticate(reason: String): Boolean {
        return suspendCancellableCoroutine { cont ->
            val executor = ContextCompat.getMainExecutor(activity)
            val prompt = BiometricPrompt(activity, executor,
                object : BiometricPrompt.AuthenticationCallback() {
                    override fun onAuthenticationSucceeded(r: BiometricPrompt.AuthenticationResult) {
                        cont.resume(true)
                    }
                    override fun onAuthenticationError(code: Int, msg: CharSequence) {
                        cont.resume(false)
                    }
                    override fun onAuthenticationFailed() { cont.resume(false) }
                })
            val info = BiometricPrompt.PromptInfo.Builder()
                .setTitle("Authenticate").setSubtitle(reason).setNegativeButtonText("Cancel").build()
            prompt.authenticate(info)
        }
    }
}

// iosMain: actual implementation using LocalAuthentication
actual class PlatformBiometric {
    actual suspend fun authenticate(reason: String): Boolean {
        return suspendCancellableCoroutine { cont ->
            val context = LAContext()
            context.evaluatePolicy(
                LAPolicy.DeviceOwnerAuthenticationWithBiometrics,
                localizedReason = reason
            ) { success, _ -> cont.resume(success) }
        }
    }
}

Avoiding Bridge Bottlenecks

  • Batch calls: don't call native 60 times/second; aggregate updates
  • Return serializable types: Map<String, Any>, List, primitives only — no custom objects across the bridge
  • Async everything: bridge calls are async; never block the JS/Dart thread waiting for native
  • EventChannel (Flutter) / EventEmitter (RN): for streaming data (BLE scan results, sensor updates) use event channels, not polling

Key Takeaways

FrameworkBridge TypeUse For
React NativeNativeModuleOne-shot calls; EventEmitter for streams
FlutterMethodChannelCalls; EventChannel for streams
KMMexpect/actualAny platform API; keeps shared code clean
AllBatch + asyncNever block the UI thread or hammer the bridge per-frame

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Bridging Native Capabilities | Android System Design | Android Engineers