Skip to Content

33c - AI 辅助原生模块开发

本文是《AI Agent 实战手册》第 33 章第 3 节。 上一节:33b-跨平台开发 | 下一节:33d-AI辅助移动端UI

概述

当跨平台框架无法满足需求时——访问蓝牙 BLE、调用生物识别硬件、集成第三方原生 SDK——你就需要编写原生模块。2025-2026 年,原生模块开发正经历三重变革:Expo Modules API 用 Swift/Kotlin 统一了双平台开发体验,React Native 新架构(TurboModules + JSI)消除了传统桥接的序列化瓶颈,而 Xcode 26 内置 Claude Agent 和 Android Studio 集成 Gemini 让 AI 可以直接在原生 IDE 中自主编写桥接代码。本节深入覆盖 iOS(Swift)和 Android(Kotlin)的原生模块开发,包括 Expo Modules API、TurboModules、Nitro Modules、Flutter Platform Channels、AI 辅助桥接代码生成的完整工作流和实战案例。


1. 原生模块开发技术全景

1.1 为什么需要原生模块

跨平台框架(React Native、Flutter)覆盖了大部分常见功能,但以下场景仍然需要原生模块:

场景示例原因
硬件访问蓝牙 BLE、NFC、UWB、LiDAR平台 API 差异大,跨平台封装不完整
性能敏感图像处理、音视频编解码、加密运算JS/Dart 层性能不足,需要原生或 C++ 实现
第三方 SDK支付 SDK、地图 SDK、推送服务SDK 仅提供原生接口,无跨平台封装
平台新特性iOS Live Activities、Android Widgets新特性发布后跨平台框架尚未支持
安全要求生物识别、密钥链/密钥库、安全飞地安全敏感操作必须在原生层执行
系统集成Siri Shortcuts、Android App Links、Widget深度系统集成需要原生代码

1.2 原生模块开发方案对比

方案框架语言性能开发体验AI 友好度适用场景
Expo Modules APIReact Native + ExpoSwift / Kotlin★★★★☆★★★★★★★★★★Expo 项目首选,现代化 API
TurboModulesReact NativeC++ / ObjC++ / Swift / Kotlin★★★★★★★★☆☆★★★★☆需要极致性能的 RN 项目
Nitro ModulesReact NativeC++ / Swift / Kotlin★★★★★★★★★☆★★★★☆高性能库开发,类型安全
Flutter Platform ChannelsFlutterSwift / Kotlin★★★★☆★★★★☆★★★★☆Flutter 项目标准方案
Flutter PigeonFlutterSwift / Kotlin★★★★☆★★★★★★★★★★Flutter 类型安全桥接
Flutter FFIFlutterC / C++ / Rust★★★★★★★★☆☆★★★☆☆高性能计算,复用 C/C++ 库
Legacy BridgeReact Native(旧架构)ObjC / Java★★☆☆☆★★☆☆☆★★★☆☆遗留项目维护

1.3 AI 辅助原生模块开发工具推荐

工具用途价格适用场景
Xcode 26 + Claude AgentiOS 原生模块开发,AI 自主编码Xcode 免费 / Claude API 按量计费Swift/SwiftUI 原生模块,iOS 专属功能
Android Studio + GeminiAndroid 原生模块开发,AI 辅助Android Studio 免费 / Gemini 免费额度Kotlin/Jetpack Compose 原生模块
Cursor跨平台原生模块开发免费 / $20/月(Pro)/ $40/月(Ultra)同时编写 Swift + Kotlin 桥接代码
Claude CodeCLI 全项目理解,自主生成桥接代码$20/月(Max 5x)/ API 按量大型原生模块重构,多文件协调
Nitro CodegenTypeScript → C++/Swift/Kotlin 代码生成免费(开源)React Native 高性能模块开发
Flutter PigeonDart API → 原生代码生成免费(开源)Flutter 类型安全桥接代码生成
Expo Modules CLIExpo 原生模块脚手架免费(开源)Expo 项目原生模块快速创建
GitHub CopilotIDE 内代码补全$10/月 / $19/月(Business)原生代码补全,API 用法提示

1.4 方案选择决策树

你需要开发原生模块? ├─ 使用 React Native + Expo? │ ├─ 是 Expo 托管工作流? │ │ └─→ Expo Modules API(首选) │ │ ✅ 现代 Swift/Kotlin API │ │ ✅ 自动桥接代码生成 │ │ ✅ 与 Expo 生态无缝集成 │ │ │ └─ 是裸 React Native 项目? │ ├─ 需要极致性能(图像/音视频)? │ │ └─→ Nitro Modules / TurboModules │ │ ✅ C++ 直接绑定 JSI │ │ ✅ 零序列化开销 │ │ │ └─ 一般原生功能? │ └─→ TurboModules(新架构) │ ✅ React Native 官方方案 │ ✅ 类型安全的 CodeGen ├─ 使用 Flutter? │ ├─ 简单方法调用? │ │ └─→ MethodChannel / Pigeon │ │ ✅ 标准方案,文档完善 │ │ │ ├─ 持续事件流(传感器/位置)? │ │ └─→ EventChannel │ │ ✅ 原生事件流到 Dart Stream │ │ │ └─ 高性能计算 / 复用 C/C++ 库? │ └─→ dart:ffi │ ✅ 直接调用 C 函数 │ ✅ 零消息传递开销 └─ 原生应用集成跨平台? ├─ 已有 iOS 应用 + 需要 Flutter 页面 │ └─→ Flutter Add-to-App └─ 已有 Android 应用 + 需要 RN 页面 └─→ React Native 嵌入原生应用

2. Expo Modules API:现代原生模块开发

2.1 Expo Modules API 概述

Expo Modules API 是 2025 年 React Native 生态中开发原生模块的推荐方案。它建立在 JSI 之上,提供了统一的 Swift 和 Kotlin API,让开发者无需接触 Objective-C 或 Java 即可创建高性能原生模块。

核心优势:

  • 现代语言:直接使用 Swift 和 Kotlin,无需 Objective-C/Java 桥接头文件
  • 统一 API:iOS 和 Android 使用几乎相同的 API 设计模式
  • 自动类型转换:JS 类型自动映射到原生类型,无需手动序列化
  • 内置生命周期管理:模块生命周期与 React Native 应用生命周期自动绑定
  • Codegen 支持:TypeScript 类型定义自动生成原生接口
  • 与 Expo 生态集成:无缝配合 EAS Build、Expo Dev Client

2.2 创建 Expo 原生模块:完整工作流

步骤 1:使用 CLI 创建模块

# 在 Expo 项目中创建本地模块 npx create-expo-module@latest --local my-native-module # 或创建独立的可发布模块 npx create-expo-module@latest my-native-module

生成的目录结构:

modules/my-native-module/ ├── android/ │ └── src/main/java/expo/modules/mynativemodule/ │ └── MyNativeModule.kt # Kotlin 实现 ├── ios/ │ └── MyNativeModule.swift # Swift 实现 ├── src/ │ ├── index.ts # TypeScript 入口 │ └── MyNativeModule.ts # TypeScript 模块定义 ├── expo-module.config.json # 模块配置 └── package.json

步骤 2:定义 TypeScript 接口

// src/MyNativeModule.ts import { NativeModule, requireNativeModule } from 'expo-modules-core'; // 声明模块接口 declare class MyNativeModuleType extends NativeModule { // 同步方法 getDeviceId(): string; // 异步方法 getBatteryLevel(): Promise<number>; // 带参数的方法 saveToKeychain(key: string, value: string): Promise<boolean>; readFromKeychain(key: string): Promise<string | null>; // 事件 addListener(eventName: string): void; removeListeners(count: number): void; } export default requireNativeModule<MyNativeModuleType>('MyNativeModule');
// src/index.ts import MyNativeModule from './MyNativeModule'; export function getDeviceId(): string { return MyNativeModule.getDeviceId(); } export async function getBatteryLevel(): Promise<number> { return await MyNativeModule.getBatteryLevel(); } export async function saveToKeychain( key: string, value: string ): Promise<boolean> { return await MyNativeModule.saveToKeychain(key, value); } export async function readFromKeychain( key: string ): Promise<string | null> { return await MyNativeModule.readFromKeychain(key); }

步骤 3:实现 iOS 原生代码(Swift)

// ios/MyNativeModule.swift import ExpoModulesCore import UIKit import LocalAuthentication import Security public class MyNativeModule: Module { // 模块定义 public func definition() -> ModuleDefinition { // 模块名称(必须与 TypeScript 中的名称一致) Name("MyNativeModule") // 同步函数:获取设备 ID Function("getDeviceId") { () -> String in return UIDevice.current.identifierForVendor?.uuidString ?? "unknown" } // 异步函数:获取电池电量 AsyncFunction("getBatteryLevel") { () -> Double in UIDevice.current.isBatteryMonitoringEnabled = true let level = UIDevice.current.batteryLevel return Double(level * 100) } // 异步函数:保存到 Keychain AsyncFunction("saveToKeychain") { (key: String, value: String) -> Bool in let data = value.data(using: .utf8)! let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: key, kSecValueData as String: data, kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly ] // 先删除已有的 SecItemDelete(query as CFDictionary) let status = SecItemAdd(query as CFDictionary, nil) return status == errSecSuccess } // 异步函数:从 Keychain 读取 AsyncFunction("readFromKeychain") { (key: String) -> String? in let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrAccount as String: key, kSecReturnData as String: true, kSecMatchLimit as String: kSecMatchLimitOne ] var result: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &result) guard status == errSecSuccess, let data = result as? Data, let value = String(data: data, encoding: .utf8) else { return nil } return value } // 事件定义 Events("onBatteryChange", "onKeychainUpdate") // 模块生命周期 OnCreate { // 模块创建时的初始化逻辑 UIDevice.current.isBatteryMonitoringEnabled = true } OnDestroy { // 模块销毁时的清理逻辑 UIDevice.current.isBatteryMonitoringEnabled = false } } }

步骤 4:实现 Android 原生代码(Kotlin)

// android/src/main/java/expo/modules/mynativemodule/MyNativeModule.kt package expo.modules.mynativemodule import android.content.Context import android.os.BatteryManager import android.os.Build import android.provider.Settings import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition import java.security.KeyStore import javax.crypto.Cipher import javax.crypto.KeyGenerator import javax.crypto.SecretKey import javax.crypto.spec.GCMParameterSpec import android.util.Base64 class MyNativeModule : Module() { override fun definition() = ModuleDefinition { // 模块名称 Name("MyNativeModule") // 同步函数:获取设备 ID Function("getDeviceId") { Settings.Secure.getString( appContext.reactContext?.contentResolver, Settings.Secure.ANDROID_ID ) ?: "unknown" } // 异步函数:获取电池电量 AsyncFunction("getBatteryLevel") { val batteryManager = appContext.reactContext ?.getSystemService(Context.BATTERY_SERVICE) as? BatteryManager val level = batteryManager ?.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) (level ?: -1).toDouble() } // 异步函数:保存到 KeyStore AsyncFunction("saveToKeychain") { key: String, value: String -> try { val sharedPrefs = appContext.reactContext ?.getSharedPreferences("secure_storage", Context.MODE_PRIVATE) sharedPrefs?.edit()?.putString(key, value)?.apply() true } catch (e: Exception) { false } } // 异步函数:从 KeyStore 读取 AsyncFunction("readFromKeychain") { key: String -> val sharedPrefs = appContext.reactContext ?.getSharedPreferences("secure_storage", Context.MODE_PRIVATE) sharedPrefs?.getString(key, null) } // 事件定义 Events("onBatteryChange", "onKeychainUpdate") // 模块生命周期 OnCreate { // 初始化逻辑 } OnDestroy { // 清理逻辑 } } }

步骤 5:在应用中使用模块

// app/settings/security.tsx import { useState, useEffect } from 'react'; import { View, Text, TextInput, Pressable, Alert } from 'react-native'; import { getDeviceId, getBatteryLevel, saveToKeychain, readFromKeychain, } from '../../modules/my-native-module'; export default function SecuritySettings() { const [deviceId, setDeviceId] = useState(''); const [battery, setBattery] = useState(0); const [secretKey, setSecretKey] = useState(''); const [storedValue, setStoredValue] = useState(''); useEffect(() => { // 同步调用 setDeviceId(getDeviceId()); // 异步调用 getBatteryLevel().then(setBattery); }, []); const handleSave = async () => { const success = await saveToKeychain('api_token', secretKey); Alert.alert(success ? '保存成功' : '保存失败'); }; const handleRead = async () => { const value = await readFromKeychain('api_token'); setStoredValue(value ?? '未找到'); }; return ( <View style={{ padding: 16 }}> <Text>设备 ID: {deviceId}</Text> <Text>电池电量: {battery}%</Text> <TextInput value={secretKey} onChangeText={setSecretKey} placeholder="输入密钥" secureTextEntry /> <Pressable onPress={handleSave}> <Text>保存到 Keychain</Text> </Pressable> <Pressable onPress={handleRead}> <Text>读取 Keychain</Text> </Pressable> <Text>存储值: {storedValue}</Text> </View> ); }

2.3 Expo Modules API 高级特性

2.3.1 原生视图组件

Expo Modules API 不仅支持函数模块,还支持创建原生视图组件:

iOS 原生视图(Swift):

// ios/MyNativeView.swift import ExpoModulesCore import UIKit import MapKit class MyMapView: ExpoView { let mapView = MKMapView() required init(appContext: AppContext? = nil) { super.init(appContext: appContext) addSubview(mapView) mapView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ mapView.topAnchor.constraint(equalTo: topAnchor), mapView.bottomAnchor.constraint(equalTo: bottomAnchor), mapView.leadingAnchor.constraint(equalTo: leadingAnchor), mapView.trailingAnchor.constraint(equalTo: trailingAnchor), ]) } func setRegion(latitude: Double, longitude: Double, delta: Double) { let region = MKCoordinateRegion( center: CLLocationCoordinate2D( latitude: latitude, longitude: longitude ), span: MKCoordinateSpan( latitudeDelta: delta, longitudeDelta: delta ) ) mapView.setRegion(region, animated: true) } } public class MyMapModule: Module { public func definition() -> ModuleDefinition { Name("MyMapView") View(MyMapView.self) { Prop("latitude") { (view: MyMapView, lat: Double) in view.setRegion( latitude: lat, longitude: view.mapView.region.center.longitude, delta: view.mapView.region.span.latitudeDelta ) } Prop("longitude") { (view: MyMapView, lng: Double) in view.setRegion( latitude: view.mapView.region.center.latitude, longitude: lng, delta: view.mapView.region.span.latitudeDelta ) } Prop("zoomLevel") { (view: MyMapView, zoom: Double) in let delta = 360.0 / pow(2.0, zoom) view.setRegion( latitude: view.mapView.region.center.latitude, longitude: view.mapView.region.center.longitude, delta: delta ) } Events("onRegionChange", "onMarkerPress") } } }

Android 原生视图(Kotlin):

// android/.../MyMapView.kt package expo.modules.mynativemodule import android.content.Context import expo.modules.kotlin.AppContext import expo.modules.kotlin.views.ExpoView import com.google.android.gms.maps.GoogleMap import com.google.android.gms.maps.MapView import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.CameraUpdateFactory class MyMapView(context: Context, appContext: AppContext) : ExpoView(context, appContext) { internal val mapView = MapView(context).also { addView(it) it.layoutParams = LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT ) } private var googleMap: GoogleMap? = null init { mapView.onCreate(null) mapView.getMapAsync { map -> googleMap = map } } fun setCenter(latitude: Double, longitude: Double, zoom: Float) { googleMap?.moveCamera( CameraUpdateFactory.newLatLngZoom( LatLng(latitude, longitude), zoom ) ) } }

2.3.2 Records(结构化数据传递)

Expo Modules API 支持使用 Records 在 JS 和原生之间传递结构化数据:

// iOS - 定义 Record struct LocationRecord: Record { @Field var latitude: Double = 0 @Field var longitude: Double = 0 @Field var altitude: Double? = nil @Field var accuracy: Double = 0 @Field var timestamp: Double = 0 } // 在模块中使用 AsyncFunction("getLocation") { () -> LocationRecord in // ... 获取位置 return LocationRecord( latitude: location.coordinate.latitude, longitude: location.coordinate.longitude, altitude: location.altitude, accuracy: location.horizontalAccuracy, timestamp: location.timestamp.timeIntervalSince1970 ) }
// Android - 定义 Record class LocationRecord : Record { @Field var latitude: Double = 0.0 @Field var longitude: Double = 0.0 @Field var altitude: Double? = null @Field var accuracy: Double = 0.0 @Field var timestamp: Double = 0.0 } // 在模块中使用 AsyncFunction("getLocation") { // ... 获取位置 LocationRecord().apply { latitude = location.latitude longitude = location.longitude altitude = location.altitude accuracy = location.accuracy.toDouble() timestamp = location.time.toDouble() } }

2.4 AI 辅助 Expo 模块开发:提示词模板

提示词模板:Expo 原生模块生成

请为 Expo 项目创建一个原生模块 [模块名称],使用 Expo Modules API: ## 功能需求 [详细描述模块需要提供的原生功能] ## 接口定义 - [方法1]: [参数] → [返回值] - [描述] - [方法2]: [参数] → [返回值] - [描述] - [事件1]: [事件数据] - [触发条件] ## 平台要求 - iOS 最低版本:[版本号] - Android 最低 API:[API 级别] - 需要的权限:[列出所需权限] ## 技术要求 1. 使用 Expo Modules API(Swift + Kotlin) 2. 所有方法包含错误处理 3. 异步操作使用 AsyncFunction 4. 结构化数据使用 Record 5. 包含完整的 TypeScript 类型定义 6. 包含使用示例代码 ## 输出文件 - ios/[ModuleName].swift - android/src/main/java/expo/modules/[name]/[ModuleName].kt - src/[ModuleName].ts(TypeScript 接口) - src/index.ts(导出入口)

提示词模板:Expo 原生视图组件生成

请为 Expo 项目创建一个原生视图组件 [组件名称]: ## 视图功能 [描述原生视图需要渲染的内容和交互] ## Props 定义 - [prop1]: [类型] - [描述] - [prop2]: [类型] - [描述] ## 事件回调 - [onEvent1]: [事件数据] - [触发条件] - [onEvent2]: [事件数据] - [触发条件] ## 平台实现 - iOS:使用 [UIKit/SwiftUI 组件] - Android:使用 [Android View/Jetpack Compose 组件] ## 要求 1. 使用 Expo Modules API 的 View 定义 2. 支持 Props 动态更新 3. 正确处理视图生命周期 4. 包含 React Native 端的 TypeScript 封装组件

3. React Native TurboModules:新架构原生模块

3.1 TurboModules 架构原理

React Native 新架构的核心变革是用 JSI(JavaScript Interface)替代了传统的 JSON Bridge。TurboModules 是新架构中原生模块的实现方式:

┌─────────────────────────────────────────────────────────┐ │ 旧架构 vs 新架构 │ ├──────────────────────┬──────────────────────────────────┤ │ 旧架构(Bridge) │ 新架构(JSI + TurboModules)│ ├──────────────────────┼──────────────────────────────────┤ │ │ │ │ JavaScript │ JavaScript │ │ │ │ │ │ │ ▼ │ ▼ │ │ JSON 序列化 │ JSI(直接 C++ 绑定) │ │ │ │ │ │ │ ▼ │ ▼ │ │ Bridge(异步队列) │ TurboModule(同步/异步) │ │ │ │ │ │ │ ▼ │ ▼ │ │ JSON 反序列化 │ 原生代码(Swift/Kotlin/C++) │ │ │ │ │ │ ▼ │ ✅ 无序列化开销 │ │ 原生代码 │ ✅ 支持同步调用 │ │ │ ✅ 懒加载(按需初始化) │ │ ❌ 序列化开销大 │ ✅ 类型安全(CodeGen) │ │ ❌ 全部异步 │ │ │ ❌ 启动时全量加载 │ │ │ ❌ 无类型安全 │ │ │ │ │ └──────────────────────┴──────────────────────────────────┘

关键概念:

  • JSI(JavaScript Interface):C++ 层,允许 JS 直接持有原生对象引用,无需序列化
  • CodeGen:从 TypeScript/Flow 类型定义自动生成 C++ 桥接代码
  • 懒加载:模块在首次调用时才初始化,减少启动时间
  • 同步调用:支持同步方法调用(旧架构全部异步)

3.2 创建 TurboModule:完整流程

步骤 1:定义 TypeScript Spec

// specs/NativeBiometricAuth.ts import type { TurboModule } from 'react-native'; import { TurboModuleRegistry } from 'react-native'; export interface Spec extends TurboModule { // 检查生物识别是否可用 isBiometricAvailable(): Promise<boolean>; // 获取支持的生物识别类型 getBiometricType(): Promise<string>; // 执行生物识别认证 authenticate(reason: string): Promise<{ success: boolean; error?: string; }>; // 检查是否已注册生物识别 isEnrolled(): Promise<boolean>; } export default TurboModuleRegistry.getEnforcing<Spec>( 'BiometricAuth' );

步骤 2:运行 CodeGen

# React Native CodeGen 会根据 Spec 自动生成 C++ 桥接代码 # 在 build 时自动执行,也可以手动触发: npx react-native codegen

CodeGen 生成的文件(自动生成,无需手动编写):

android/app/build/generated/source/codegen/ ├── java/com/facebook/fbreact/specs/ │ └── NativeBiometricAuthSpec.java # Java 接口 └── jni/ ├── NativeBiometricAuthSpec.h # C++ 头文件 └── NativeBiometricAuthSpec.cpp # C++ 实现 ios/build/generated/ios/ ├── RNBiometricAuthSpec.h # ObjC++ 头文件 └── RNBiometricAuthSpec.mm # ObjC++ 实现

步骤 3:实现 iOS 原生代码(Swift)

// ios/BiometricAuth/BiometricAuthModule.swift import Foundation import LocalAuthentication @objc(BiometricAuth) class BiometricAuthModule: NSObject { @objc func isBiometricAvailable( _ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ) { let context = LAContext() var error: NSError? let available = context.canEvaluatePolicy( .deviceOwnerAuthenticationWithBiometrics, error: &error ) resolve(available) } @objc func getBiometricType( _ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ) { let context = LAContext() var error: NSError? guard context.canEvaluatePolicy( .deviceOwnerAuthenticationWithBiometrics, error: &error ) else { resolve("none") return } switch context.biometryType { case .faceID: resolve("faceId") case .touchID: resolve("touchId") case .opticID: resolve("opticId") @unknown default: resolve("unknown") } } @objc func authenticate( _ reason: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ) { let context = LAContext() context.localizedFallbackTitle = "使用密码" context.evaluatePolicy( .deviceOwnerAuthenticationWithBiometrics, localizedReason: reason ) { success, error in DispatchQueue.main.async { if success { resolve(["success": true]) } else { let errorMessage = error?.localizedDescription ?? "认证失败" resolve(["success": false, "error": errorMessage]) } } } } @objc func isEnrolled( _ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock ) { let context = LAContext() var error: NSError? let enrolled = context.canEvaluatePolicy( .deviceOwnerAuthenticationWithBiometrics, error: &error ) resolve(enrolled) } @objc static func requiresMainQueueSetup() -> Bool { return false } }

步骤 4:实现 Android 原生代码(Kotlin)

// android/app/src/main/java/com/myapp/biometric/BiometricAuthModule.kt package com.myapp.biometric import android.os.Build import androidx.biometric.BiometricManager import androidx.biometric.BiometricPrompt import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentActivity import com.facebook.react.bridge.* import com.facebook.react.module.annotations.ReactModule @ReactModule(name = BiometricAuthModule.NAME) class BiometricAuthModule(reactContext: ReactApplicationContext) : NativeBiometricAuthSpec(reactContext) { companion object { const val NAME = "BiometricAuth" } override fun getName() = NAME override fun isBiometricAvailable(promise: Promise) { val biometricManager = BiometricManager.from(reactApplicationContext) val canAuthenticate = biometricManager.canAuthenticate( BiometricManager.Authenticators.BIOMETRIC_STRONG ) promise.resolve(canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) } override fun getBiometricType(promise: Promise) { val biometricManager = BiometricManager.from(reactApplicationContext) val canAuthenticate = biometricManager.canAuthenticate( BiometricManager.Authenticators.BIOMETRIC_STRONG ) when (canAuthenticate) { BiometricManager.BIOMETRIC_SUCCESS -> { // Android 不区分指纹和面部,统一返回 biometric promise.resolve("biometric") } else -> promise.resolve("none") } } override fun authenticate(reason: String, promise: Promise) { val activity = currentActivity as? FragmentActivity if (activity == null) { promise.resolve( Arguments.createMap().apply { putBoolean("success", false) putString("error", "Activity not available") } ) return } val executor = ContextCompat.getMainExecutor(reactApplicationContext) val callback = object : BiometricPrompt.AuthenticationCallback() { override fun onAuthenticationSucceeded( result: BiometricPrompt.AuthenticationResult ) { promise.resolve( Arguments.createMap().apply { putBoolean("success", true) } ) } override fun onAuthenticationError( errorCode: Int, errString: CharSequence ) { promise.resolve( Arguments.createMap().apply { putBoolean("success", false) putString("error", errString.toString()) } ) } override fun onAuthenticationFailed() { // 单次失败不 resolve,等待用户重试或取消 } } val promptInfo = BiometricPrompt.PromptInfo.Builder() .setTitle("生物识别认证") .setSubtitle(reason) .setNegativeButtonText("取消") .setAllowedAuthenticators( BiometricManager.Authenticators.BIOMETRIC_STRONG ) .build() activity.runOnUiThread { BiometricPrompt(activity, executor, callback) .authenticate(promptInfo) } } override fun isEnrolled(promise: Promise) { val biometricManager = BiometricManager.from(reactApplicationContext) val canAuthenticate = biometricManager.canAuthenticate( BiometricManager.Authenticators.BIOMETRIC_STRONG ) promise.resolve(canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) } }

3.3 AI 辅助 TurboModule 开发:提示词模板

提示词模板:TurboModule Spec 生成

请为 React Native 新架构生成一个 TurboModule: ## 模块名称 [ModuleName] ## 功能描述 [详细描述模块功能] ## 方法定义 1. [方法名]([参数列表]) → [返回类型]:[描述] 2. [方法名]([参数列表]) → [返回类型]:[描述] ## 要求 1. 生成 TypeScript Spec 文件(specs/Native[ModuleName].ts) 2. 生成 iOS 实现(Swift,使用 LocalAuthentication/CoreBluetooth 等框架) 3. 生成 Android 实现(Kotlin,使用 AndroidX 库) 4. 所有异步方法使用 Promise 5. 包含完整的错误处理 6. 遵循 React Native 新架构 CodeGen 规范 7. 包含 JS 端的封装层和使用示例 ## 平台特定注意事项 - iOS:[iOS 特定要求] - Android:[Android 特定要求]

4. Nitro Modules:极致性能的原生模块

4.1 Nitro Modules 概述

Nitro Modules 是由 mrousavy(VisionCamera 作者)开发的高性能原生模块框架。它通过静态编译的 C++ 绑定层实现了接近零开销的 JS-原生通信。

Nitro vs TurboModules vs Expo Modules 对比:

特性Nitro ModulesTurboModulesExpo Modules API
性能最快(静态 C++ 绑定)快(JSI 动态绑定)快(基于 JSI)
代码生成Nitrogen(TS → C++/Swift/Kotlin)CodeGen(TS → C++)内置类型转换
语言支持C++ / Swift / KotlinC++ / ObjC++ / Swift / KotlinSwift / Kotlin
学习曲线中等较高
依赖react-native-nitro-modulesreact-native 核心expo-modules-core
适用场景高性能库(相机/视频/图像)通用原生模块Expo 项目通用模块
社区快速增长React Native 官方Expo 官方

4.2 Nitro Modules 开发流程

步骤 1:定义 TypeScript 接口

// src/specs/ImageProcessor.nitro.ts import { HybridObject } from 'react-native-nitro-modules'; // 定义图像处理器接口 export interface ImageProcessor extends HybridObject<{ ios: 'swift'; android: 'kotlin'; }> { // 属性 readonly supportedFormats: string[]; quality: number; // 同步方法 getImageSize(path: string): { width: number; height: number }; // 异步方法 compressImage( inputPath: string, outputPath: string, quality: number ): Promise<{ size: number; path: string }>; resizeImage( inputPath: string, width: number, height: number ): Promise<string>; applyFilter( inputPath: string, filter: 'grayscale' | 'sepia' | 'blur' | 'sharpen', intensity: number ): Promise<string>; // 批量处理 batchProcess( paths: string[], operations: Array<{ type: 'compress' | 'resize' | 'filter'; params: Record<string, unknown>; }> ): Promise<string[]>; }

步骤 2:运行 Nitrogen 代码生成

# 安装 Nitrogen npm install nitro-codegen --save-dev # 运行代码生成 npx nitro-codegen # Nitrogen 会生成以下文件: # - C++ 桥接代码(自动生成) # - Swift 协议定义(自动生成) # - Kotlin 接口定义(自动生成)

步骤 3:实现 Swift 原生代码

// ios/ImageProcessor.swift import NitroModules import UIKit import CoreImage class HybridImageProcessor: HybridImageProcessorSpec { // 属性 var supportedFormats: [String] { return ["jpeg", "png", "heic", "webp"] } var quality: Double = 0.8 // 同步方法 func getImageSize(path: String) throws -> ImageSize { guard let image = UIImage(contentsOfFile: path) else { throw RuntimeError.error( withMessage: "无法加载图片: \(path)" ) } return ImageSize( width: Double(image.size.width), height: Double(image.size.height) ) } // 异步方法 func compressImage( inputPath: String, outputPath: String, quality: Double ) async throws -> CompressResult { guard let image = UIImage(contentsOfFile: inputPath) else { throw RuntimeError.error( withMessage: "无法加载图片" ) } guard let data = image.jpegData( compressionQuality: CGFloat(quality) ) else { throw RuntimeError.error( withMessage: "压缩失败" ) } try data.write(to: URL(fileURLWithPath: outputPath)) return CompressResult( size: Double(data.count), path: outputPath ) } func resizeImage( inputPath: String, width: Double, height: Double ) async throws -> String { guard let image = UIImage(contentsOfFile: inputPath) else { throw RuntimeError.error( withMessage: "无法加载图片" ) } let size = CGSize(width: width, height: height) let renderer = UIGraphicsImageRenderer(size: size) let resized = renderer.image { _ in image.draw(in: CGRect(origin: .zero, size: size)) } let outputPath = inputPath .replacingOccurrences(of: ".jpg", with: "_resized.jpg") if let data = resized.jpegData( compressionQuality: CGFloat(quality) ) { try data.write(to: URL(fileURLWithPath: outputPath)) } return outputPath } func applyFilter( inputPath: String, filter: String, intensity: Double ) async throws -> String { guard let inputImage = CIImage( contentsOf: URL(fileURLWithPath: inputPath) ) else { throw RuntimeError.error( withMessage: "无法加载图片" ) } let ciFilter: CIFilter? switch filter { case "grayscale": ciFilter = CIFilter(name: "CIColorMonochrome") ciFilter?.setValue(inputImage, forKey: kCIInputImageKey) ciFilter?.setValue( CIColor(red: 0.7, green: 0.7, blue: 0.7), forKey: kCIInputColorKey ) ciFilter?.setValue(intensity, forKey: kCIInputIntensityKey) case "sepia": ciFilter = CIFilter(name: "CISepiaTone") ciFilter?.setValue(inputImage, forKey: kCIInputImageKey) ciFilter?.setValue(intensity, forKey: kCIInputIntensityKey) case "blur": ciFilter = CIFilter(name: "CIGaussianBlur") ciFilter?.setValue(inputImage, forKey: kCIInputImageKey) ciFilter?.setValue(intensity * 10, forKey: kCIInputRadiusKey) case "sharpen": ciFilter = CIFilter(name: "CISharpenLuminance") ciFilter?.setValue(inputImage, forKey: kCIInputImageKey) ciFilter?.setValue(intensity, forKey: kCIInputSharpnessKey) default: throw RuntimeError.error( withMessage: "不支持的滤镜: \(filter)" ) } guard let outputImage = ciFilter?.outputImage else { throw RuntimeError.error(withMessage: "滤镜处理失败") } let context = CIContext() let outputPath = inputPath .replacingOccurrences(of: ".jpg", with: "_filtered.jpg") let outputURL = URL(fileURLWithPath: outputPath) try context.writeJPEGRepresentation( of: outputImage, to: outputURL, colorSpace: CGColorSpaceCreateDeviceRGB() ) return outputPath } }

4.3 AI 辅助 Nitro 模块开发:提示词模板

提示词模板:Nitro Module 生成

请为 React Native 创建一个 Nitro Module [模块名称]: ## 功能需求 [详细描述高性能原生功能需求] ## 接口定义(HybridObject) - 属性:[列出只读/可写属性] - 同步方法:[列出同步方法及签名] - 异步方法:[列出异步方法及签名] ## 性能要求 - [描述性能敏感的操作] - 预期吞吐量:[如 30fps 视频处理] ## 要求 1. 生成 TypeScript 接口(.nitro.ts) 2. 生成 Swift 实现(HybridObject) 3. 生成 Kotlin 实现(HybridObject) 4. 使用 async/await 处理异步操作 5. 包含完整的错误处理(throws) 6. 性能敏感操作在后台线程执行 7. 包含 JS 端使用示例

5. Flutter Platform Channels:原生通信机制

5.1 Flutter 三种 Channel 类型

Flutter 提供三种 Platform Channel 用于 Dart 与原生代码通信:

┌─────────────────────────────────────────────────────────────┐ │ Flutter Platform Channels 架构 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ Dart 层 │ │ ┌──────────────┐ ┌──────────────┐ ┌───────────────────┐ │ │ │ MethodChannel │ │ EventChannel │ │BasicMessageChannel│ │ │ └──────┬───────┘ └──────┬───────┘ └────────┬──────────┘ │ │ │ │ │ │ │ ───────┼────────────────┼───────────────────┼────────── │ │ │ 消息编解码(StandardMessageCodec)│ │ │ ───────┼────────────────┼───────────────────┼────────── │ │ │ │ │ │ │ 平台层(iOS / Android) │ │ ┌──────┴───────┐ ┌──────┴───────┐ ┌────────┴──────────┐ │ │ │FlutterMethod │ │FlutterEvent │ │FlutterBasicMessage│ │ │ │ Channel │ │ Channel │ │ Channel │ │ │ └──────────────┘ └──────────────┘ └───────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘
Channel 类型通信模式适用场景示例
MethodChannel请求-响应(双向)一次性方法调用获取电池电量、打开相机、保存文件
EventChannel持续事件流(原生→Dart)持续数据推送传感器数据、位置更新、蓝牙状态
BasicMessageChannel自由格式消息(双向)自定义协议通信复杂数据交换、自定义序列化

5.2 MethodChannel 实战:相机模块

Dart 端实现

// lib/services/camera_service.dart import 'dart:async'; import 'package:flutter/services.dart'; class CameraService { static const _channel = MethodChannel('com.myapp/camera'); /// 检查相机权限 Future<bool> checkPermission() async { try { final result = await _channel.invokeMethod<bool>('checkPermission'); return result ?? false; } on PlatformException catch (e) { throw CameraException('权限检查失败: ${e.message}'); } } /// 请求相机权限 Future<bool> requestPermission() async { try { final result = await _channel.invokeMethod<bool>('requestPermission'); return result ?? false; } on PlatformException catch (e) { throw CameraException('权限请求失败: ${e.message}'); } } /// 拍照 Future<String?> takePicture({ bool frontCamera = false, int quality = 85, }) async { try { final result = await _channel.invokeMethod<String>( 'takePicture', { 'frontCamera': frontCamera, 'quality': quality, }, ); return result; } on PlatformException catch (e) { throw CameraException('拍照失败: ${e.message}'); } } /// 录制视频 Future<String?> recordVideo({ int maxDuration = 60, String quality = 'high', }) async { try { final result = await _channel.invokeMethod<String>( 'recordVideo', { 'maxDuration': maxDuration, 'quality': quality, }, ); return result; } on PlatformException catch (e) { throw CameraException('录制失败: ${e.message}'); } } } class CameraException implements Exception { final String message; CameraException(this.message); @override String toString() => 'CameraException: $message'; }

iOS 原生实现(Swift)

// ios/Runner/CameraPlugin.swift import Flutter import UIKit import AVFoundation import Photos class CameraPlugin: NSObject, FlutterPlugin { static func register(with registrar: FlutterPluginRegistrar) { let channel = FlutterMethodChannel( name: "com.myapp/camera", binaryMessenger: registrar.messenger() ) let instance = CameraPlugin() registrar.addMethodCallDelegate(instance, channel: channel) } func handle( _ call: FlutterMethodCall, result: @escaping FlutterResult ) { switch call.method { case "checkPermission": checkPermission(result: result) case "requestPermission": requestPermission(result: result) case "takePicture": let args = call.arguments as? [String: Any] ?? [:] takePicture(args: args, result: result) case "recordVideo": let args = call.arguments as? [String: Any] ?? [:] recordVideo(args: args, result: result) default: result(FlutterMethodNotImplemented) } } private func checkPermission(result: @escaping FlutterResult) { let status = AVCaptureDevice.authorizationStatus(for: .video) result(status == .authorized) } private func requestPermission(result: @escaping FlutterResult) { AVCaptureDevice.requestAccess(for: .video) { granted in DispatchQueue.main.async { result(granted) } } } private func takePicture( args: [String: Any], result: @escaping FlutterResult ) { let frontCamera = args["frontCamera"] as? Bool ?? false let quality = args["quality"] as? Int ?? 85 guard let viewController = UIApplication.shared .keyWindow?.rootViewController else { result(FlutterError( code: "NO_VIEW_CONTROLLER", message: "无法获取视图控制器", details: nil )) return } let picker = UIImagePickerController() picker.sourceType = .camera picker.cameraDevice = frontCamera ? .front : .rear picker.allowsEditing = false // 使用闭包处理拍照结果 let delegate = ImagePickerDelegate { image in if let image = image, let data = image.jpegData( compressionQuality: CGFloat(quality) / 100.0 ) { let tempDir = NSTemporaryDirectory() let fileName = "photo_\(Int(Date().timeIntervalSince1970)).jpg" let filePath = (tempDir as NSString) .appendingPathComponent(fileName) do { try data.write(to: URL(fileURLWithPath: filePath)) result(filePath) } catch { result(FlutterError( code: "SAVE_ERROR", message: "保存照片失败", details: error.localizedDescription )) } } else { result(nil) } } picker.delegate = delegate // 保持 delegate 引用 objc_setAssociatedObject( picker, "delegate", delegate, .OBJC_ASSOCIATION_RETAIN_NONATOMIC ) viewController.present(picker, animated: true) } private func recordVideo( args: [String: Any], result: @escaping FlutterResult ) { let maxDuration = args["maxDuration"] as? Int ?? 60 let quality = args["quality"] as? String ?? "high" // 视频录制实现... result(FlutterMethodNotImplemented) } } // 图片选择器代理 class ImagePickerDelegate: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate { let completion: (UIImage?) -> Void init(completion: @escaping (UIImage?) -> Void) { self.completion = completion } func imagePickerController( _ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [ UIImagePickerController.InfoKey: Any ] ) { let image = info[.originalImage] as? UIImage picker.dismiss(animated: true) { self.completion(image) } } func imagePickerControllerDidCancel( _ picker: UIImagePickerController ) { picker.dismiss(animated: true) { self.completion(nil) } } }

Android 原生实现(Kotlin)

// android/app/src/main/kotlin/.../CameraPlugin.kt package com.myapp.camera import android.Manifest import android.app.Activity import android.content.Intent import android.content.pm.PackageManager import android.net.Uri import android.os.Environment import android.provider.MediaStore import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.PluginRegistry import java.io.File import java.text.SimpleDateFormat import java.util.* class CameraPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware, PluginRegistry.ActivityResultListener, PluginRegistry.RequestPermissionsResultListener { private lateinit var channel: MethodChannel private var activity: Activity? = null private var pendingResult: MethodChannel.Result? = null private var currentPhotoPath: String? = null companion object { private const val CHANNEL_NAME = "com.myapp/camera" private const val REQUEST_CAMERA_PERMISSION = 1001 private const val REQUEST_TAKE_PICTURE = 1002 private const val REQUEST_RECORD_VIDEO = 1003 } override fun onAttachedToEngine( binding: FlutterPlugin.FlutterPluginBinding ) { channel = MethodChannel(binding.binaryMessenger, CHANNEL_NAME) channel.setMethodCallHandler(this) } override fun onDetachedFromEngine( binding: FlutterPlugin.FlutterPluginBinding ) { channel.setMethodCallHandler(null) } override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { "checkPermission" -> checkPermission(result) "requestPermission" -> requestPermission(result) "takePicture" -> { val frontCamera = call.argument<Boolean>("frontCamera") ?: false val quality = call.argument<Int>("quality") ?: 85 takePicture(frontCamera, quality, result) } "recordVideo" -> { val maxDuration = call.argument<Int>("maxDuration") ?: 60 val quality = call.argument<String>("quality") ?: "high" recordVideo(maxDuration, quality, result) } else -> result.notImplemented() } } private fun checkPermission(result: MethodChannel.Result) { val granted = ContextCompat.checkSelfPermission( activity!!, Manifest.permission.CAMERA ) == PackageManager.PERMISSION_GRANTED result.success(granted) } private fun requestPermission(result: MethodChannel.Result) { pendingResult = result ActivityCompat.requestPermissions( activity!!, arrayOf(Manifest.permission.CAMERA), REQUEST_CAMERA_PERMISSION ) } private fun takePicture( frontCamera: Boolean, quality: Int, result: MethodChannel.Result ) { pendingResult = result val photoFile = createImageFile() currentPhotoPath = photoFile.absolutePath val photoUri = FileProvider.getUriForFile( activity!!, "${activity!!.packageName}.fileprovider", photoFile ) val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply { putExtra(MediaStore.EXTRA_OUTPUT, photoUri) if (frontCamera) { putExtra( "android.intent.extras.CAMERA_FACING", android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT ) } } activity?.startActivityForResult(intent, REQUEST_TAKE_PICTURE) } private fun recordVideo( maxDuration: Int, quality: String, result: MethodChannel.Result ) { pendingResult = result val intent = Intent(MediaStore.ACTION_VIDEO_CAPTURE).apply { putExtra(MediaStore.EXTRA_DURATION_LIMIT, maxDuration) putExtra( MediaStore.EXTRA_VIDEO_QUALITY, if (quality == "high") 1 else 0 ) } activity?.startActivityForResult(intent, REQUEST_RECORD_VIDEO) } private fun createImageFile(): File { val timeStamp = SimpleDateFormat( "yyyyMMdd_HHmmss", Locale.getDefault() ).format(Date()) val storageDir = activity!!.getExternalFilesDir( Environment.DIRECTORY_PICTURES ) return File.createTempFile("PHOTO_${timeStamp}_", ".jpg", storageDir) } // ActivityAware 接口实现 override fun onAttachedToActivity(binding: ActivityPluginBinding) { activity = binding.activity binding.addActivityResultListener(this) binding.addRequestPermissionsResultListener(this) } override fun onDetachedFromActivity() { activity = null } override fun onReattachedToActivityForConfigChanges( binding: ActivityPluginBinding ) { activity = binding.activity } override fun onDetachedFromActivityForConfigChanges() { activity = null } // 处理权限请求结果 override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<out String>, grantResults: IntArray ): Boolean { if (requestCode == REQUEST_CAMERA_PERMISSION) { val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED pendingResult?.success(granted) pendingResult = null return true } return false } // 处理 Activity 结果 override fun onActivityResult( requestCode: Int, resultCode: Int, data: Intent? ): Boolean { when (requestCode) { REQUEST_TAKE_PICTURE -> { if (resultCode == Activity.RESULT_OK) { pendingResult?.success(currentPhotoPath) } else { pendingResult?.success(null) } pendingResult = null return true } REQUEST_RECORD_VIDEO -> { if (resultCode == Activity.RESULT_OK) { val videoUri = data?.data pendingResult?.success(videoUri?.path) } else { pendingResult?.success(null) } pendingResult = null return true } } return false } }

5.3 EventChannel 实战:传感器数据流

Dart 端实现

// lib/services/sensor_service.dart import 'dart:async'; import 'package:flutter/services.dart'; class SensorService { static const _eventChannel = EventChannel('com.myapp/sensors'); static const _methodChannel = MethodChannel('com.myapp/sensors/control'); Stream<SensorData>? _sensorStream; /// 获取传感器数据流 Stream<SensorData> get sensorStream { _sensorStream ??= _eventChannel .receiveBroadcastStream() .map((event) { final map = Map<String, dynamic>.from(event as Map); return SensorData.fromMap(map); }); return _sensorStream!; } /// 启动传感器监听 Future<void> startListening({ int intervalMs = 100, List<String> sensors = const ['accelerometer', 'gyroscope'], }) async { await _methodChannel.invokeMethod('startListening', { 'intervalMs': intervalMs, 'sensors': sensors, }); } /// 停止传感器监听 Future<void> stopListening() async { await _methodChannel.invokeMethod('stopListening'); } } class SensorData { final String type; final double x; final double y; final double z; final int timestamp; SensorData({ required this.type, required this.x, required this.y, required this.z, required this.timestamp, }); factory SensorData.fromMap(Map<String, dynamic> map) { return SensorData( type: map['type'] as String, x: (map['x'] as num).toDouble(), y: (map['y'] as num).toDouble(), z: (map['z'] as num).toDouble(), timestamp: map['timestamp'] as int, ); } }

iOS EventChannel 实现(Swift)

// ios/Runner/SensorPlugin.swift import Flutter import CoreMotion class SensorPlugin: NSObject, FlutterPlugin, FlutterStreamHandler { private let motionManager = CMMotionManager() private var eventSink: FlutterEventSink? private var updateInterval: TimeInterval = 0.1 static func register(with registrar: FlutterPluginRegistrar) { // EventChannel let eventChannel = FlutterEventChannel( name: "com.myapp/sensors", binaryMessenger: registrar.messenger() ) // MethodChannel(控制) let methodChannel = FlutterMethodChannel( name: "com.myapp/sensors/control", binaryMessenger: registrar.messenger() ) let instance = SensorPlugin() eventChannel.setStreamHandler(instance) methodChannel.setMethodCallHandler(instance.handleMethod) } // FlutterStreamHandler - 开始监听 func onListen( withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink ) -> FlutterError? { self.eventSink = events startSensors() return nil } // FlutterStreamHandler - 停止监听 func onCancel(withArguments arguments: Any?) -> FlutterError? { eventSink = nil stopSensors() return nil } private func startSensors() { // 加速度计 if motionManager.isAccelerometerAvailable { motionManager.accelerometerUpdateInterval = updateInterval motionManager.startAccelerometerUpdates( to: .main ) { [weak self] data, error in guard let data = data, let sink = self?.eventSink else { return } sink([ "type": "accelerometer", "x": data.acceleration.x, "y": data.acceleration.y, "z": data.acceleration.z, "timestamp": Int(Date().timeIntervalSince1970 * 1000), ]) } } // 陀螺仪 if motionManager.isGyroAvailable { motionManager.gyroUpdateInterval = updateInterval motionManager.startGyroUpdates(to: .main) { [weak self] data, error in guard let data = data, let sink = self?.eventSink else { return } sink([ "type": "gyroscope", "x": data.rotationRate.x, "y": data.rotationRate.y, "z": data.rotationRate.z, "timestamp": Int(Date().timeIntervalSince1970 * 1000), ]) } } } private func stopSensors() { motionManager.stopAccelerometerUpdates() motionManager.stopGyroUpdates() } func handleMethod( call: FlutterMethodCall, result: @escaping FlutterResult ) { switch call.method { case "startListening": let args = call.arguments as? [String: Any] ?? [:] let intervalMs = args["intervalMs"] as? Int ?? 100 updateInterval = TimeInterval(intervalMs) / 1000.0 startSensors() result(nil) case "stopListening": stopSensors() result(nil) default: result(FlutterMethodNotImplemented) } } }

5.4 Flutter Pigeon:类型安全的桥接代码生成

Pigeon 是 Flutter 官方推荐的代码生成工具,从 Dart API 定义自动生成原生桥接代码,消除手动编写 Platform Channel 的样板代码。

定义 Pigeon API

// pigeons/camera_api.dart import 'package:pigeon/pigeon.dart'; @ConfigurePigeon(PigeonOptions( dartOut: 'lib/src/generated/camera_api.g.dart', kotlinOut: 'android/app/src/main/kotlin/com/myapp/CameraApi.g.kt', kotlinOptions: KotlinOptions(package: 'com.myapp'), swiftOut: 'ios/Runner/CameraApi.g.swift', )) /// 相机配置 class CameraConfig { final bool frontCamera; final int quality; final int maxWidth; final int maxHeight; CameraConfig({ required this.frontCamera, required this.quality, required this.maxWidth, required this.maxHeight, }); } /// 拍照结果 class PhotoResult { final String path; final int width; final int height; final int fileSize; PhotoResult({ required this.path, required this.width, required this.height, required this.fileSize, }); } /// 相机权限状态 enum PermissionStatus { granted, denied, restricted, permanentlyDenied, } /// 原生 → Dart 的 API(原生实现,Dart 调用) @HostApi() abstract class CameraHostApi { @async PermissionStatus checkPermission(); @async PermissionStatus requestPermission(); @async PhotoResult takePicture(CameraConfig config); @async String recordVideo(int maxDurationSeconds); bool isCameraAvailable(); } /// Dart → 原生的回调(Dart 实现,原生调用) @FlutterApi() abstract class CameraFlutterApi { void onCameraError(String errorCode, String message); void onRecordingProgress(int durationMs); }

运行代码生成

# 生成桥接代码 dart run pigeon --input pigeons/camera_api.dart # 生成的文件: # - lib/src/generated/camera_api.g.dart (Dart 接口) # - ios/Runner/CameraApi.g.swift (Swift 接口) # - android/.../CameraApi.g.kt (Kotlin 接口)

实现 Swift 端

// ios/Runner/CameraApiImpl.swift import Flutter class CameraHostApiImpl: CameraHostApi { func checkPermission( completion: @escaping (Result<PermissionStatus, Error>) -> Void ) { let status = AVCaptureDevice.authorizationStatus(for: .video) switch status { case .authorized: completion(.success(.granted)) case .denied: completion(.success(.denied)) case .restricted: completion(.success(.restricted)) case .notDetermined: completion(.success(.denied)) @unknown default: completion(.success(.denied)) } } func requestPermission( completion: @escaping (Result<PermissionStatus, Error>) -> Void ) { AVCaptureDevice.requestAccess(for: .video) { granted in DispatchQueue.main.async { completion(.success(granted ? .granted : .denied)) } } } func takePicture( config: CameraConfig, completion: @escaping (Result<PhotoResult, Error>) -> Void ) { // 实现拍照逻辑... let result = PhotoResult( path: "/tmp/photo.jpg", width: Int64(config.maxWidth), height: Int64(config.maxHeight), fileSize: 1024 * 100 ) completion(.success(result)) } func recordVideo( maxDurationSeconds: Int64, completion: @escaping (Result<String, Error>) -> Void ) { // 实现录制逻辑... completion(.success("/tmp/video.mp4")) } func isCameraAvailable() throws -> Bool { return UIImagePickerController.isSourceTypeAvailable(.camera) } } // 在 AppDelegate 中注册 // CameraHostApiSetup.setUp( // binaryMessenger: controller.binaryMessenger, // api: CameraHostApiImpl() // )

5.5 Flutter FFI:高性能原生调用

对于性能极其敏感的场景(图像处理、加密、音频处理),Flutter 提供 dart:ffi 直接调用 C/C++ 函数:

// lib/src/native/image_ffi.dart import 'dart:ffi'; import 'dart:io'; import 'package:ffi/ffi.dart'; // 定义 C 函数签名 typedef NativeResizeImage = Int32 Function( Pointer<Utf8> inputPath, Pointer<Utf8> outputPath, Int32 width, Int32 height, Int32 quality, ); typedef DartResizeImage = int Function( Pointer<Utf8> inputPath, Pointer<Utf8> outputPath, int width, int height, int quality, ); class ImageFFI { late final DynamicLibrary _lib; late final DartResizeImage _resizeImage; ImageFFI() { // 加载原生库 if (Platform.isAndroid) { _lib = DynamicLibrary.open('libimage_processor.so'); } else if (Platform.isIOS) { _lib = DynamicLibrary.process(); } // 绑定函数 _resizeImage = _lib .lookupFunction<NativeResizeImage, DartResizeImage>( 'resize_image', ); } /// 调整图片大小(高性能,直接调用 C 代码) int resizeImage( String inputPath, String outputPath, int width, int height, { int quality = 85, }) { final inputPtr = inputPath.toNativeUtf8(); final outputPtr = outputPath.toNativeUtf8(); try { return _resizeImage( inputPtr, outputPtr, width, height, quality, ); } finally { calloc.free(inputPtr); calloc.free(outputPtr); } } }

5.6 AI 辅助 Flutter 原生模块开发:提示词模板

提示词模板:Flutter Platform Channel 模块生成

请为 Flutter 应用创建一个原生模块 [模块名称],使用 Platform Channels: ## 功能需求 [详细描述需要的原生功能] ## Channel 类型选择 - MethodChannel:[列出请求-响应式方法] - EventChannel:[列出持续事件流] ## 接口定义 - [方法1]([参数]) → [返回值]:[描述] - [事件流1]:[事件数据格式] - [触发条件] ## 要求 1. 生成 Dart 服务类(lib/services/[name]_service.dart) 2. 生成 iOS 实现(Swift,FlutterPlugin) 3. 生成 Android 实现(Kotlin,FlutterPlugin) 4. 包含完整的错误处理(PlatformException) 5. EventChannel 使用 Stream API 6. 在 AppDelegate / MainActivity 中注册插件 7. 包含使用示例(Widget 中调用) ## 平台权限 - iOS Info.plist 需要的权限描述 - Android AndroidManifest.xml 需要的权限

提示词模板:Flutter Pigeon API 生成

请使用 Flutter Pigeon 为 [功能名称] 生成类型安全的桥接代码: ## API 定义 ### 数据模型 - [Model1]: { [字段列表] } - [Model2]: { [字段列表] } ### HostApi(原生实现,Dart 调用) - [方法1]([参数]) → [返回值] - [方法2]([参数]) → [返回值] ### FlutterApi(Dart 实现,原生回调) - [回调1]([参数]) - [回调2]([参数]) ## 要求 1. 生成 Pigeon 定义文件(pigeons/[name]_api.dart) 2. 生成 Swift 实现类 3. 生成 Kotlin 实现类 4. 生成 Dart 封装服务类 5. 包含 @ConfigurePigeon 配置 6. 异步方法使用 @async 注解 7. 包含枚举类型定义

6. AI 辅助原生模块开发工具深度指南

6.1 Xcode 26 + Claude Agent:iOS 原生模块开发

2025-2026 年 iOS 原生模块开发的最大变革是 Xcode 26 系列对 AI 的深度集成:

Xcode 26 AI 能力演进:

版本发布时间AI 能力
Xcode 262025 年 9 月AI Coding Intelligence:代码补全、重构建议、文档生成
Xcode 26.32026 年 2 月Claude Agent SDK + OpenAI Codex 集成,MCP 支持,自主编码

Xcode 26.3 的 Agentic 编码能力:

  • 自主规划:AI Agent 可以浏览项目结构、理解架构,自主规划实现方案
  • 多文件编辑:Agent 可以同时修改 Swift 源文件、Info.plist、Entitlements 等
  • 构建与测试:Agent 可以触发构建、读取错误日志、自动修复编译错误
  • MCP 集成:通过 Model Context Protocol 连接外部工具和数据源
  • 里程碑控制:开发者设置检查点,Agent 在关键节点暂停等待确认

使用 Xcode 26.3 开发原生模块的工作流:

1. 在 Xcode 中打开项目 2. 激活 Claude Agent(Xcode → Preferences → AI → Claude Agent) 3. 描述原生模块需求: "创建一个 Swift 原生模块,封装 CoreBluetooth 框架, 提供以下功能: - 扫描附近的 BLE 设备 - 连接到指定设备 - 读写 GATT 特征值 - 监听特征值变化通知 模块需要: - 使用 Expo Modules API 暴露给 React Native - 处理蓝牙权限请求 - 支持后台蓝牙模式 - 包含完整的错误处理" 4. Agent 自主执行: - 分析项目结构 - 创建 Swift 文件 - 配置 Info.plist 权限 - 编写 Expo Module 定义 - 构建验证 - 修复编译错误 5. 开发者在里程碑处审查代码

6.2 Android Studio + Gemini:Android 原生模块开发

Android Studio Narwhal 系列深度集成了 Gemini AI 助手:

Gemini 在 Android Studio 中的能力:

功能描述适用场景
代码补全上下文感知的 Kotlin 代码补全编写原生模块方法实现
Compose 预览生成自动生成 Jetpack Compose 预览原生视图组件开发
自然语言 UI 转换用自然语言描述 UI 变更快速调整原生视图
JourneysAI 驱动的自动化测试测试原生模块功能
Version Upgrade Agent自动处理 SDK 版本升级升级 Android API 级别
项目上下文附加图片和项目文件到 prompt复杂原生模块开发

使用 Gemini 开发 Android 原生模块:

在 Android Studio 的 Gemini 面板中: "请帮我创建一个 Kotlin 原生模块,封装 Android 的推送通知功能: 1. 使用 Firebase Cloud Messaging (FCM) 2. 支持前台和后台通知处理 3. 支持通知渠道(Notification Channels)管理 4. 支持自定义通知样式(大图、进度条、操作按钮) 5. 使用 Expo Modules API 暴露给 React Native 6. 包含 AndroidManifest.xml 权限配置 7. 处理 Android 13+ 的通知权限请求 请生成完整的 Kotlin 代码和配置文件。"

6.3 Cursor + Claude Code:跨平台原生模块开发

对于需要同时编写 iOS 和 Android 原生代码的场景,Cursor 和 Claude Code 是最佳选择:

Cursor 规则文件:原生模块开发

--- description: 原生模块开发规则 globs: ["ios/**/*.swift", "android/**/*.kt", "modules/**/*"] --- # 原生模块开发规范 ## 通用规则 - iOS 使用 Swift 5.9+,禁止 Objective-C - Android 使用 Kotlin 1.9+,禁止 Java - 所有公开方法必须有文档注释 - 错误处理使用平台原生异常机制 ## Expo Modules API 规则 - 模块名称使用 PascalCase - 异步方法使用 AsyncFunction - 结构化数据使用 Record - 事件使用 Events 声明 ## 线程安全 - iOS:UI 操作必须在主线程(DispatchQueue.main) - Android:UI 操作必须在主线程(runOnUiThread) - 耗时操作必须在后台线程执行 - 使用 async/await 而非回调嵌套 ## 内存管理 - iOS:注意 ARC 循环引用,使用 [weak self] - Android:注意 Activity 泄漏,使用 WeakReference - 大数据传输使用文件路径而非内存拷贝 ## 权限处理 - 所有权限请求必须有用户友好的说明文字 - 处理权限被永久拒绝的情况(引导用户到设置) - iOS:在 Info.plist 中声明权限用途描述 - Android:在 AndroidManifest.xml 中声明权限

Claude Code CLAUDE.md 配置:

# CLAUDE.md(原生模块项目) ## 项目类型 React Native + Expo 项目,包含自定义原生模块。 ## 原生模块位置 - modules/:本地 Expo 模块 - ios/:iOS 原生代码 - android/:Android 原生代码 ## 开发命令 - `npx expo run:ios`:构建并运行 iOS - `npx expo run:android`:构建并运行 Android - `npx expo prebuild --clean`:重新生成原生项目 - `npx create-expo-module@latest --local [name]`:创建新模块 ## 原生模块开发规则 1. 优先使用 Expo Modules API 2. iOS 使用 Swift,Android 使用 Kotlin 3. 所有方法包含错误处理 4. 异步操作使用 AsyncFunction 5. 测试时在真机上验证(模拟器可能不支持某些硬件功能) ## 注意事项 - 修改原生代码后需要重新构建(不支持热重载) - iOS 模拟器不支持:相机、蓝牙、NFC、推送通知 - Android 模拟器不支持:NFC、部分传感器

7. 实战案例:蓝牙 BLE 原生模块

7.1 需求分析

构建一个蓝牙 BLE 原生模块,支持以下功能:

  • 扫描附近的 BLE 设备
  • 连接到指定设备
  • 发现服务和特征值
  • 读写特征值
  • 监听特征值变化通知

7.2 使用 Expo Modules API 实现

TypeScript 接口定义

// modules/ble-module/src/BleModule.ts import { NativeModule, requireNativeModule, } from 'expo-modules-core'; import { EventEmitter } from 'expo-modules-core'; export interface BleDevice { id: string; name: string | null; rssi: number; advertisementData?: Record<string, unknown>; } export interface BleService { uuid: string; characteristics: BleCharacteristic[]; } export interface BleCharacteristic { uuid: string; properties: string[]; value?: string; // Base64 编码 } declare class BleModuleType extends NativeModule { // 蓝牙状态 isBluetoothEnabled(): Promise<boolean>; requestBluetoothPermission(): Promise<boolean>; // 扫描 startScan(serviceUUIDs?: string[]): Promise<void>; stopScan(): Promise<void>; // 连接 connect(deviceId: string): Promise<void>; disconnect(deviceId: string): Promise<void>; // 服务发现 discoverServices(deviceId: string): Promise<BleService[]>; // 读写 readCharacteristic( deviceId: string, serviceUUID: string, characteristicUUID: string ): Promise<string>; // Base64 writeCharacteristic( deviceId: string, serviceUUID: string, characteristicUUID: string, value: string // Base64 ): Promise<void>; // 通知 startNotification( deviceId: string, serviceUUID: string, characteristicUUID: string ): Promise<void>; stopNotification( deviceId: string, serviceUUID: string, characteristicUUID: string ): Promise<void>; } const BleModule = requireNativeModule<BleModuleType>('BleModule'); export const bleEmitter = new EventEmitter(BleModule); export default BleModule;

iOS BLE 实现(Swift)

// modules/ble-module/ios/BleModule.swift import ExpoModulesCore import CoreBluetooth public class BleModule: Module { private var centralManager: CBCentralManager? private var peripherals: [String: CBPeripheral] = [:] private var delegate: BleDelegate? public func definition() -> ModuleDefinition { Name("BleModule") Events( "onDeviceFound", "onConnectionStateChange", "onCharacteristicValueChange", "onScanComplete" ) OnCreate { delegate = BleDelegate(module: self) centralManager = CBCentralManager( delegate: delegate, queue: DispatchQueue.main ) } AsyncFunction("isBluetoothEnabled") { () -> Bool in return self.centralManager?.state == .poweredOn } AsyncFunction("requestBluetoothPermission") { () -> Bool in if #available(iOS 13.1, *) { let status = CBCentralManager.authorization return status == .allowedAlways } return true } AsyncFunction("startScan") { (serviceUUIDs: [String]?) in let uuids = serviceUUIDs?.map { CBUUID(string: $0) } self.centralManager?.scanForPeripherals( withServices: uuids, options: [ CBCentralManagerScanOptionAllowDuplicatesKey: false ] ) } AsyncFunction("stopScan") { self.centralManager?.stopScan() } AsyncFunction("connect") { (deviceId: String) in guard let peripheral = self.peripherals[deviceId] else { throw BleError.deviceNotFound(deviceId) } self.centralManager?.connect(peripheral, options: nil) } AsyncFunction("disconnect") { (deviceId: String) in guard let peripheral = self.peripherals[deviceId] else { throw BleError.deviceNotFound(deviceId) } self.centralManager?.cancelPeripheralConnection(peripheral) } AsyncFunction("discoverServices") { (deviceId: String) -> [[String: Any]] in guard let peripheral = self.peripherals[deviceId] else { throw BleError.deviceNotFound(deviceId) } peripheral.discoverServices(nil) // 等待服务发现完成(简化示例) try await Task.sleep(nanoseconds: 2_000_000_000) return peripheral.services?.map { service in [ "uuid": service.uuid.uuidString, "characteristics": service.characteristics?.map { char in [ "uuid": char.uuid.uuidString, "properties": self.characteristicProperties(char), ] } ?? [], ] } ?? [] } AsyncFunction("readCharacteristic") { ( deviceId: String, serviceUUID: String, characteristicUUID: String ) -> String in guard let characteristic = self.findCharacteristic( deviceId: deviceId, serviceUUID: serviceUUID, characteristicUUID: characteristicUUID ) else { throw BleError.characteristicNotFound(characteristicUUID) } guard let peripheral = self.peripherals[deviceId] else { throw BleError.deviceNotFound(deviceId) } peripheral.readValue(for: characteristic) // 等待读取完成 try await Task.sleep(nanoseconds: 1_000_000_000) return characteristic.value? .base64EncodedString() ?? "" } AsyncFunction("writeCharacteristic") { ( deviceId: String, serviceUUID: String, characteristicUUID: String, value: String ) in guard let characteristic = self.findCharacteristic( deviceId: deviceId, serviceUUID: serviceUUID, characteristicUUID: characteristicUUID ) else { throw BleError.characteristicNotFound(characteristicUUID) } guard let peripheral = self.peripherals[deviceId], let data = Data(base64Encoded: value) else { throw BleError.invalidData } peripheral.writeValue( data, for: characteristic, type: .withResponse ) } AsyncFunction("startNotification") { ( deviceId: String, serviceUUID: String, characteristicUUID: String ) in guard let characteristic = self.findCharacteristic( deviceId: deviceId, serviceUUID: serviceUUID, characteristicUUID: characteristicUUID ) else { throw BleError.characteristicNotFound(characteristicUUID) } guard let peripheral = self.peripherals[deviceId] else { throw BleError.deviceNotFound(deviceId) } peripheral.setNotifyValue(true, for: characteristic) } AsyncFunction("stopNotification") { ( deviceId: String, serviceUUID: String, characteristicUUID: String ) in guard let characteristic = self.findCharacteristic( deviceId: deviceId, serviceUUID: serviceUUID, characteristicUUID: characteristicUUID ) else { throw BleError.characteristicNotFound(characteristicUUID) } guard let peripheral = self.peripherals[deviceId] else { throw BleError.deviceNotFound(deviceId) } peripheral.setNotifyValue(false, for: characteristic) } OnDestroy { self.centralManager?.stopScan() for peripheral in self.peripherals.values { self.centralManager?.cancelPeripheralConnection(peripheral) } } } // 辅助方法 func addPeripheral(_ peripheral: CBPeripheral) { peripherals[peripheral.identifier.uuidString] = peripheral } private func findCharacteristic( deviceId: String, serviceUUID: String, characteristicUUID: String ) -> CBCharacteristic? { guard let peripheral = peripherals[deviceId] else { return nil } let service = peripheral.services?.first { $0.uuid.uuidString == serviceUUID } return service?.characteristics?.first { $0.uuid.uuidString == characteristicUUID } } private func characteristicProperties( _ char: CBCharacteristic ) -> [String] { var props: [String] = [] if char.properties.contains(.read) { props.append("read") } if char.properties.contains(.write) { props.append("write") } if char.properties.contains(.notify) { props.append("notify") } if char.properties.contains(.indicate) { props.append("indicate") } return props } } // 错误定义 enum BleError: Error { case deviceNotFound(String) case characteristicNotFound(String) case invalidData case bluetoothDisabled } // CBCentralManager 代理 class BleDelegate: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate { weak var module: BleModule? init(module: BleModule) { self.module = module } func centralManagerDidUpdateState( _ central: CBCentralManager ) { // 蓝牙状态变化 } func centralManager( _ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber ) { module?.addPeripheral(peripheral) module?.sendEvent("onDeviceFound", [ "id": peripheral.identifier.uuidString, "name": peripheral.name ?? "", "rssi": RSSI.intValue, ]) } func centralManager( _ central: CBCentralManager, didConnect peripheral: CBPeripheral ) { peripheral.delegate = self module?.sendEvent("onConnectionStateChange", [ "deviceId": peripheral.identifier.uuidString, "state": "connected", ]) } func centralManager( _ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error? ) { module?.sendEvent("onConnectionStateChange", [ "deviceId": peripheral.identifier.uuidString, "state": "disconnected", ]) } func peripheral( _ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error? ) { module?.sendEvent("onCharacteristicValueChange", [ "deviceId": peripheral.identifier.uuidString, "serviceUUID": characteristic.service?.uuid.uuidString ?? "", "characteristicUUID": characteristic.uuid.uuidString, "value": characteristic.value?.base64EncodedString() ?? "", ]) } }

7.3 React Native 端使用示例

// app/bluetooth/scanner.tsx import { useState, useEffect, useCallback } from 'react'; import { View, Text, FlatList, Pressable, StyleSheet, ActivityIndicator, Alert, } from 'react-native'; import BleModule, { bleEmitter } from '../../modules/ble-module'; import type { BleDevice } from '../../modules/ble-module'; export default function BluetoothScanner() { const [devices, setDevices] = useState<BleDevice[]>([]); const [scanning, setScanning] = useState(false); const [connectedId, setConnectedId] = useState<string | null>(null); useEffect(() => { // 监听设备发现事件 const deviceSub = bleEmitter.addListener( 'onDeviceFound', (device: BleDevice) => { setDevices(prev => { const exists = prev.find(d => d.id === device.id); if (exists) { return prev.map(d => d.id === device.id ? device : d ); } return [...prev, device]; }); } ); // 监听连接状态变化 const connSub = bleEmitter.addListener( 'onConnectionStateChange', ({ deviceId, state }) => { if (state === 'connected') { setConnectedId(deviceId); Alert.alert('已连接', `设备 ${deviceId} 已连接`); } else { setConnectedId(null); } } ); return () => { deviceSub.remove(); connSub.remove(); }; }, []); const handleStartScan = useCallback(async () => { const enabled = await BleModule.isBluetoothEnabled(); if (!enabled) { Alert.alert('蓝牙未开启', '请在设置中开启蓝牙'); return; } const hasPermission = await BleModule.requestBluetoothPermission(); if (!hasPermission) { Alert.alert('权限不足', '请授予蓝牙权限'); return; } setDevices([]); setScanning(true); await BleModule.startScan(); // 10 秒后自动停止扫描 setTimeout(async () => { await BleModule.stopScan(); setScanning(false); }, 10000); }, []); const handleConnect = useCallback(async (deviceId: string) => { try { await BleModule.stopScan(); setScanning(false); await BleModule.connect(deviceId); } catch (error) { Alert.alert('连接失败', String(error)); } }, []); const renderDevice = ({ item }: { item: BleDevice }) => ( <Pressable style={[ styles.deviceItem, item.id === connectedId && styles.connectedDevice, ]} onPress={() => handleConnect(item.id)} accessibilityLabel={`蓝牙设备 ${item.name || '未知设备'}`} accessibilityRole="button" > <View style={styles.deviceInfo}> <Text style={styles.deviceName}> {item.name || '未知设备'} </Text> <Text style={styles.deviceId}>{item.id}</Text> </View> <Text style={styles.rssi}> {item.rssi} dBm </Text> </Pressable> ); return ( <View style={styles.container}> <Pressable style={[styles.scanButton, scanning && styles.scanningButton]} onPress={scanning ? BleModule.stopScan : handleStartScan} accessibilityLabel={scanning ? '停止扫描' : '开始扫描'} > {scanning && <ActivityIndicator color="#fff" />} <Text style={styles.scanButtonText}> {scanning ? '扫描中...' : '开始扫描'} </Text> </Pressable> <FlatList data={devices} keyExtractor={item => item.id} renderItem={renderDevice} ListEmptyComponent={ <Text style={styles.emptyText}> {scanning ? '正在搜索设备...' : '点击上方按钮开始扫描'} </Text> } /> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, padding: 16 }, scanButton: { backgroundColor: '#007AFF', padding: 16, borderRadius: 12, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', gap: 8, marginBottom: 16, minHeight: 52, }, scanningButton: { backgroundColor: '#FF3B30' }, scanButtonText: { color: '#fff', fontSize: 16, fontWeight: '600', }, deviceItem: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 16, borderRadius: 8, backgroundColor: '#f5f5f5', marginBottom: 8, minHeight: 64, }, connectedDevice: { backgroundColor: '#e8f5e9', borderWidth: 1, borderColor: '#4CAF50', }, deviceInfo: { flex: 1 }, deviceName: { fontSize: 16, fontWeight: '500' }, deviceId: { fontSize: 12, color: '#666', marginTop: 4 }, rssi: { fontSize: 14, color: '#999' }, emptyText: { textAlign: 'center', color: '#999', marginTop: 32, }, });

8. 实战案例:推送通知原生模块

8.1 AI 辅助生成推送通知模块

以下展示使用 Claude Code 生成完整推送通知模块的工作流:

Claude Code 提示词:

请为我的 Expo 项目创建一个推送通知原生模块,使用 Expo Modules API: ## 功能需求 1. 请求通知权限(iOS + Android 13+) 2. 获取设备推送 Token(APNs + FCM) 3. 显示本地通知(支持自定义样式) 4. 处理通知点击事件 5. 管理通知渠道(Android) 6. 支持静默推送 7. 角标管理(iOS) ## 技术要求 - iOS:使用 UserNotifications 框架 - Android:使用 Firebase Cloud Messaging - 事件通知使用 Expo Events - 所有方法包含错误处理

AI 生成的模块结构:

modules/push-notification/ ├── android/ │ └── src/main/java/expo/modules/pushnotification/ │ ├── PushNotificationModule.kt │ ├── NotificationChannelManager.kt │ └── FCMService.kt ├── ios/ │ ├── PushNotificationModule.swift │ └── NotificationDelegate.swift ├── src/ │ ├── index.ts │ ├── PushNotificationModule.ts │ └── types.ts └── expo-module.config.json

8.2 关键实现片段

iOS 推送 Token 获取

// ios/PushNotificationModule.swift(关键片段) import ExpoModulesCore import UserNotifications import UIKit public class PushNotificationModule: Module { public func definition() -> ModuleDefinition { Name("PushNotification") Events( "onNotificationReceived", "onNotificationResponse", "onTokenRefresh" ) AsyncFunction("requestPermission") { () -> [String: Any] in let center = UNUserNotificationCenter.current() let granted = try await center.requestAuthorization( options: [.alert, .badge, .sound] ) if granted { await MainActor.run { UIApplication.shared.registerForRemoteNotifications() } } let settings = await center.notificationSettings() return [ "granted": granted, "alertSetting": settings.alertSetting == .enabled, "badgeSetting": settings.badgeSetting == .enabled, "soundSetting": settings.soundSetting == .enabled, ] } AsyncFunction("getDeviceToken") { () -> String? in // Token 通过 AppDelegate 回调获取并缓存 return UserDefaults.standard.string(forKey: "apnsToken") } AsyncFunction("scheduleLocalNotification") { (config: NotificationConfig) in let content = UNMutableNotificationContent() content.title = config.title content.body = config.body content.sound = .default if let badge = config.badge { content.badge = NSNumber(value: badge) } if let data = config.data { content.userInfo = data } let trigger: UNNotificationTrigger if let delaySeconds = config.delaySeconds { trigger = UNTimeIntervalNotificationTrigger( timeInterval: TimeInterval(delaySeconds), repeats: false ) } else { trigger = UNTimeIntervalNotificationTrigger( timeInterval: 1, repeats: false ) } let request = UNNotificationRequest( identifier: config.id ?? UUID().uuidString, content: content, trigger: trigger ) try await UNUserNotificationCenter.current() .add(request) } AsyncFunction("setBadgeCount") { (count: Int) in await MainActor.run { UIApplication.shared.applicationIconBadgeNumber = count } } AsyncFunction("cancelAllNotifications") { UNUserNotificationCenter.current() .removeAllPendingNotificationRequests() UNUserNotificationCenter.current() .removeAllDeliveredNotifications() } } } // Record 定义 struct NotificationConfig: Record { @Field var id: String? = nil @Field var title: String = "" @Field var body: String = "" @Field var badge: Int? = nil @Field var data: [String: String]? = nil @Field var delaySeconds: Int? = nil }

Android 推送 Token 获取

// android/.../PushNotificationModule.kt(关键片段) package expo.modules.pushnotification import android.Manifest import android.app.NotificationChannel import android.app.NotificationManager import android.content.Context import android.content.pm.PackageManager import android.os.Build import androidx.core.app.ActivityCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import com.google.firebase.messaging.FirebaseMessaging import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition import kotlinx.coroutines.tasks.await class PushNotificationModule : Module() { companion object { private const val DEFAULT_CHANNEL_ID = "default" private const val DEFAULT_CHANNEL_NAME = "默认通知" } override fun definition() = ModuleDefinition { Name("PushNotification") Events( "onNotificationReceived", "onNotificationResponse", "onTokenRefresh" ) OnCreate { createDefaultChannel() } AsyncFunction("requestPermission") { val context = appContext.reactContext ?: return@AsyncFunction mapOf( "granted" to false ) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val granted = ActivityCompat.checkSelfPermission( context, Manifest.permission.POST_NOTIFICATIONS ) == PackageManager.PERMISSION_GRANTED if (!granted) { // 需要通过 Activity 请求权限 // 简化示例,实际需要使用 ActivityResultLauncher } mapOf("granted" to granted) } else { mapOf("granted" to true) } } AsyncFunction("getDeviceToken") { try { val token = FirebaseMessaging.getInstance().token.await() token } catch (e: Exception) { null } } AsyncFunction("scheduleLocalNotification") { config: NotificationConfig -> val context = appContext.reactContext ?: return@AsyncFunction val builder = NotificationCompat.Builder( context, config.channelId ?: DEFAULT_CHANNEL_ID ) .setSmallIcon(android.R.drawable.ic_dialog_info) .setContentTitle(config.title) .setContentText(config.body) .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setAutoCancel(true) val notificationId = config.id?.hashCode() ?: System.currentTimeMillis().toInt() if (ActivityCompat.checkSelfPermission( context, Manifest.permission.POST_NOTIFICATIONS ) == PackageManager.PERMISSION_GRANTED ) { NotificationManagerCompat.from(context) .notify(notificationId, builder.build()) } } AsyncFunction("createNotificationChannel") { channelId: String, channelName: String, importance: Int -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( channelId, channelName, importance ) val manager = appContext.reactContext ?.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager manager?.createNotificationChannel(channel) } } AsyncFunction("cancelAllNotifications") { val context = appContext.reactContext ?: return@AsyncFunction NotificationManagerCompat.from(context).cancelAll() } } private fun createDefaultChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( DEFAULT_CHANNEL_ID, DEFAULT_CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT ) val manager = appContext.reactContext ?.getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager manager?.createNotificationChannel(channel) } } } // Record 定义 class NotificationConfig : expo.modules.kotlin.records.Record { @expo.modules.kotlin.records.Field var id: String? = null @expo.modules.kotlin.records.Field var title: String = "" @expo.modules.kotlin.records.Field var body: String = "" @expo.modules.kotlin.records.Field var channelId: String? = null @expo.modules.kotlin.records.Field var data: Map<String, String>? = null }

9. SwiftUI 与 Jetpack Compose 集成

9.1 在 React Native 中嵌入 SwiftUI 视图

Expo Modules API 支持将 SwiftUI 视图暴露为 React Native 组件:

// ios/SwiftUIBridge.swift import ExpoModulesCore import SwiftUI // SwiftUI 视图 struct GradientCard: View { let title: String let subtitle: String let colors: [Color] var body: some View { VStack(alignment: .leading, spacing: 8) { Text(title) .font(.headline) .foregroundColor(.white) Text(subtitle) .font(.subheadline) .foregroundColor(.white.opacity(0.8)) } .padding(20) .frame(maxWidth: .infinity, alignment: .leading) .background( LinearGradient( colors: colors, startPoint: .topLeading, endPoint: .bottomTrailing ) ) .cornerRadius(16) .shadow(radius: 8) } } // 桥接到 Expo class GradientCardView: ExpoView { private var hostingController: UIHostingController<GradientCard>? var title: String = "" { didSet { updateView() } } var subtitle: String = "" { didSet { updateView() } } var gradientColors: [String] = ["#007AFF", "#5856D6"] { didSet { updateView() } } required init(appContext: AppContext? = nil) { super.init(appContext: appContext) setupView() } private func setupView() { let swiftUIView = GradientCard( title: title, subtitle: subtitle, colors: gradientColors.map { Color(hex: $0) } ) let controller = UIHostingController(rootView: swiftUIView) controller.view.backgroundColor = .clear addSubview(controller.view) controller.view.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ controller.view.topAnchor.constraint(equalTo: topAnchor), controller.view.bottomAnchor.constraint(equalTo: bottomAnchor), controller.view.leadingAnchor.constraint(equalTo: leadingAnchor), controller.view.trailingAnchor.constraint(equalTo: trailingAnchor), ]) hostingController = controller } private func updateView() { let swiftUIView = GradientCard( title: title, subtitle: subtitle, colors: gradientColors.map { Color(hex: $0) } ) hostingController?.rootView = swiftUIView } } // 模块定义 public class GradientCardModule: Module { public func definition() -> ModuleDefinition { Name("GradientCard") View(GradientCardView.self) { Prop("title") { (view: GradientCardView, value: String) in view.title = value } Prop("subtitle") { (view: GradientCardView, value: String) in view.subtitle = value } Prop("colors") { (view: GradientCardView, value: [String]) in view.gradientColors = value } } } } // Color 扩展 extension Color { init(hex: String) { let hex = hex.trimmingCharacters(in: CharacterSet(charactersIn: "#")) let scanner = Scanner(string: hex) var rgbValue: UInt64 = 0 scanner.scanHexInt64(&rgbValue) let r = Double((rgbValue & 0xFF0000) >> 16) / 255.0 let g = Double((rgbValue & 0x00FF00) >> 8) / 255.0 let b = Double(rgbValue & 0x0000FF) / 255.0 self.init(red: r, green: g, blue: b) } }

9.2 在 React Native 中嵌入 Jetpack Compose 视图

// android/.../ComposeCardView.kt package expo.modules.gradientcard import android.content.Context import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import expo.modules.kotlin.AppContext import expo.modules.kotlin.views.ExpoView class ComposeCardView( context: Context, appContext: AppContext ) : ExpoView(context, appContext) { private val composeView = ComposeView(context) var title by mutableStateOf("") var subtitle by mutableStateOf("") var gradientColors by mutableStateOf( listOf("#007AFF", "#5856D6") ) init { addView(composeView) composeView.layoutParams = LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT ) composeView.setContent { GradientCardComposable( title = title, subtitle = subtitle, colors = gradientColors.map { parseColor(it) } ) } } private fun parseColor(hex: String): Color { val colorString = hex.removePrefix("#") return Color(android.graphics.Color.parseColor("#$colorString")) } } @Composable fun GradientCardComposable( title: String, subtitle: String, colors: List<Color> ) { Box( modifier = Modifier .fillMaxWidth() .shadow(8.dp, RoundedCornerShape(16.dp)) .clip(RoundedCornerShape(16.dp)) .background( Brush.linearGradient(colors) ) .padding(20.dp) ) { Column { Text( text = title, color = Color.White, fontSize = 18.sp, fontWeight = FontWeight.Bold ) Spacer(modifier = Modifier.height(8.dp)) Text( text = subtitle, color = Color.White.copy(alpha = 0.8f), fontSize = 14.sp ) } } }

9.3 在 React Native 中使用原生视图

// components/GradientCard.tsx import { requireNativeViewManager } from 'expo-modules-core'; import { ViewProps } from 'react-native'; interface GradientCardProps extends ViewProps { title: string; subtitle: string; colors?: string[]; } const NativeGradientCard = requireNativeViewManager('GradientCard'); export function GradientCard({ title, subtitle, colors = ['#007AFF', '#5856D6'], style, ...props }: GradientCardProps) { return ( <NativeGradientCard title={title} subtitle={subtitle} colors={colors} style={[{ height: 120 }, style]} {...props} /> ); } // 使用示例 export function DashboardScreen() { return ( <View style={{ padding: 16, gap: 12 }}> <GradientCard title="今日收入" subtitle="¥12,580.00" colors={['#667eea', '#764ba2']} /> <GradientCard title="活跃用户" subtitle="3,842 人" colors={['#f093fb', '#f5576c']} /> <GradientCard title="订单数量" subtitle="156 笔" colors={['#4facfe', '#00f2fe']} /> </View> ); }

9.4 AI 辅助 SwiftUI/Compose 集成:提示词模板

请为 React Native + Expo 项目创建一个原生视图组件, 同时使用 SwiftUI(iOS)和 Jetpack Compose(Android)实现: ## 视图名称 [ComponentName] ## 视觉设计 [描述视图的外观和布局] ## Props - [prop1]: [类型] - [描述] - [prop2]: [类型] - [描述] ## 交互 - [交互1 描述] - [交互2 描述] ## 要求 1. iOS 使用 SwiftUI 实现,通过 UIHostingController 桥接 2. Android 使用 Jetpack Compose 实现,通过 ComposeView 桥接 3. 使用 Expo Modules API 的 View 定义 4. Props 变化时视图自动更新 5. 支持 Dark Mode 6. 包含 React Native 端的 TypeScript 封装 7. 包含使用示例

10. 原生模块测试策略

10.1 测试层次

┌─────────────────────────────────────────────────┐ │ 原生模块测试金字塔 │ ├─────────────────────────────────────────────────┤ │ │ │ ┌─────────────────┐ │ │ │ E2E 测试 │ ← 真机/模拟器 │ │ │ (Detox/Maestro)│ 完整功能验证 │ │ └────────┬────────┘ │ │ │ │ │ ┌───────────┴───────────┐ │ │ │ 集成测试 │ ← JS + 原生 │ │ │ (Jest + 原生测试) │ 桥接验证 │ │ └───────────┬───────────┘ │ │ │ │ │ ┌──────────────┴──────────────┐ │ │ │ 单元测试 │ ← 纯原生 │ │ │ (XCTest / JUnit) │ 逻辑验证 │ │ └─────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────┘

10.2 iOS 单元测试(XCTest)

// ios/Tests/BleModuleTests.swift import XCTest @testable import BleModule class BleModuleTests: XCTestCase { var module: BleModule! override func setUp() { super.setUp() module = BleModule() } func testCharacteristicPropertiesParsing() { // 测试特征值属性解析 // ... } func testDeviceIdFormat() { // 测试设备 ID 格式 let deviceId = module.getDeviceId() XCTAssertFalse(deviceId.isEmpty) XCTAssertTrue( deviceId.contains("-"), "设备 ID 应为 UUID 格式" ) } }

10.3 Android 单元测试(JUnit)

// android/src/test/java/.../BleModuleTest.kt package expo.modules.ble import org.junit.Assert.* import org.junit.Before import org.junit.Test class BleModuleTest { @Test fun `test notification config parsing`() { val config = NotificationConfig().apply { title = "测试通知" body = "这是一条测试通知" } assertEquals("测试通知", config.title) assertEquals("这是一条测试通知", config.body) assertNull(config.channelId) } }

10.4 E2E 测试(Maestro)

# .maestro/test_bluetooth.yaml appId: com.myapp --- - launchApp - tapOn: "蓝牙扫描" # 等待权限弹窗 - tapOn: text: "允许" optional: true # 验证扫描开始 - assertVisible: "扫描中..." # 等待设备出现 - waitForAnimationToEnd - assertVisible: text: ".*设备.*" regex: true # 停止扫描 - tapOn: "扫描中..." - assertVisible: "开始扫描"

10.5 AI 辅助测试生成:提示词模板

请为原生模块 [模块名称] 生成测试代码: ## 测试范围 - [列出需要测试的方法和功能] ## 测试类型 1. iOS 单元测试(XCTest) 2. Android 单元测试(JUnit + Mockk) 3. JS 集成测试(Jest) 4. E2E 测试(Maestro) ## 要求 - 覆盖正常路径和错误路径 - Mock 硬件相关的 API(蓝牙、相机等) - 测试权限被拒绝的场景 - 测试并发调用的安全性 - 包含性能基准测试(可选)

避坑指南

❌ 常见错误

  1. 线程安全问题:在后台线程更新 UI

    • 问题:原生模块的回调可能在后台线程执行,直接更新 UI 会导致崩溃或未定义行为
    • 正确做法:
      • iOS:使用 DispatchQueue.main.async { }@MainActor
      • Android:使用 activity.runOnUiThread { }withContext(Dispatchers.Main)
    • AI 提示:在 prompt 中明确要求”所有 UI 操作必须在主线程执行”
  2. 内存泄漏:未正确释放原生资源

    • 问题:原生模块持有的资源(蓝牙连接、传感器监听、定时器)未在模块销毁时释放
    • 正确做法:
      • OnDestroy 生命周期中释放所有资源
      • iOS:使用 [weak self] 避免闭包循环引用
      • Android:使用 WeakReference 避免 Activity 泄漏
    • AI 提示:在 prompt 中要求”包含完整的生命周期管理和资源清理”
  3. 平台 API 版本差异:未检查 API 可用性

    • 问题:使用了高版本 API 但未检查设备系统版本,导致低版本设备崩溃
    • 正确做法:
      • iOS:使用 if #available(iOS 16, *) { } 检查
      • Android:使用 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) 检查
    • AI 提示:在 prompt 中指定”最低支持 iOS [版本] / Android API [级别]”
  4. 权限处理不完整:未处理权限被永久拒绝

    • 问题:只处理了权限授予和拒绝,未处理”不再询问”(Android)或”受限”(iOS)状态
    • 正确做法:
      • 检测权限被永久拒绝时,引导用户到系统设置页面
      • 提供清晰的权限用途说明
      • iOS:在 Info.plist 中添加 NSxxxUsageDescription
      • Android 13+:运行时请求 POST_NOTIFICATIONS 权限
  5. 数据序列化错误:传递不支持的类型

    • 问题:在 JS 和原生之间传递了不支持的数据类型(如 Date、Buffer、自定义类)
    • 正确做法:
      • 使用基础类型(string、number、boolean、array、object)
      • 二进制数据使用 Base64 编码
      • 日期使用 ISO 8601 字符串或 Unix 时间戳
      • 复杂对象使用 Record(Expo)或 ReadableMap(RN)
  6. 异步操作未处理取消

    • 问题:用户离开页面后异步操作仍在执行,回调时组件已卸载
    • 正确做法:
      • 支持操作取消(AbortController 模式)
      • 在模块销毁时取消所有进行中的操作
      • React Native 端使用 useEffect 清理函数
  7. Expo Modules 与裸 RN 模块混淆

    • 问题:在 Expo 托管工作流中使用了需要原生链接的裸 RN 模块
    • 正确做法:
      • Expo 项目优先使用 Expo Modules API
      • 需要裸 RN 模块时,使用 expo prebuild 切换到裸工作流
      • 或使用 Config Plugin 自动配置原生依赖
  8. CodeGen 类型不匹配

    • 问题:TypeScript Spec 中的类型与原生实现不匹配,导致运行时崩溃
    • 正确做法:
      • 修改 Spec 后重新运行 CodeGen
      • 确保原生方法签名与生成的接口完全一致
      • 使用 CI 自动验证类型一致性
  9. 后台模式未正确配置

    • 问题:蓝牙、位置等功能在应用进入后台后停止工作
    • 正确做法:
      • iOS:在 Info.plist 中配置 UIBackgroundModes
      • Android:使用 Foreground Service
      • 测试时验证后台行为

✅ 最佳实践

  1. 优先使用 Expo Modules API:除非有特殊性能需求,否则 Expo Modules API 是最简单、最现代的原生模块开发方案
  2. TypeScript 接口先行:先定义 TypeScript 接口,再实现原生代码,确保 API 设计合理
  3. 双平台同步开发:iOS 和 Android 实现同步进行,避免一个平台完成后发现 API 设计不适合另一个平台
  4. 真机测试:硬件相关功能(蓝牙、NFC、相机)必须在真机上测试
  5. 错误信息本地化:错误消息使用用户友好的本地化文字,而非原生错误码
  6. 版本兼容性矩阵:维护一个平台版本兼容性矩阵,明确每个功能的最低支持版本
  7. AI 生成后必须审查:AI 生成的原生代码必须经过人工审查,特别关注线程安全、内存管理和权限处理
  8. 使用 Config Plugin:对于需要修改原生配置(Info.plist、AndroidManifest.xml)的模块,使用 Expo Config Plugin 自动化配置

相关资源与延伸阅读

  1. Expo Modules API 官方文档  — Expo 原生模块开发的权威指南,包含完整的 API 参考和教程
  2. React Native 新架构 TurboModules 指南  — React Native 工作组维护的 TurboModules 官方文档
  3. Nitro Modules 官方文档  — 高性能原生模块框架的完整文档和 API 参考
  4. Flutter Platform Channels 官方文档  — Flutter 原生通信机制的官方指南
  5. Flutter Pigeon 包  — Flutter 类型安全桥接代码生成工具
  6. Callstack: Bridgeless Native Development  — Nitro 和 TurboModules 的深度对比分析(2025)
  7. Xcode 26.3 Claude Agent SDK 集成  — Anthropic 官方发布的 Xcode AI 集成公告
  8. Android Studio Gemini AI 功能  — Google I/O 2025 发布的 Android Studio AI 更新
  9. Flutter 无缝互操作路线图  — Flutter 团队关于原生互操作未来方向的官方博文
  10. Expo Modules API 设计考量  — 理解 Expo Modules API 设计决策的官方文档

参考来源


📖 返回 总览与导航 | 上一节:33b-跨平台开发 | 下一节:33d-AI辅助移动端UI

Last updated on