Setup and context
Rork Max is often praised for letting you build complex apps without code — but its real power lies in native platform integration. By connecting to platform-specific features like HealthKit, ARKit, CameraX, and NFC, you can ship high-value apps in days that would be impossible with any other no-code tool.
That said, native integration comes with genuine pitfalls:
- iOS and Android use completely different APIs and mental models for the same features
- Asking for device permissions at the wrong moment leads to users denying access permanently
- Skipping memory leak prevention and version compatibility checks causes crashes in production
- Misunderstanding Android's threading model hides serious bugs that only surface at scale
This guide walks you through real, working native integrations in Rork Max, complete with code examples and testing strategies. Everything you need to build healthcare apps, AR experiences, and payment apps that actually generate revenue.
Rork Max Native Integration Architecture
There are three main ways to access native capabilities in Rork Max.
① Expo Modules API (Recommended) Expo's module extension API lets you call Swift (iOS) and Kotlin (Android) from TypeScript. Rork Max officially supports this layer for native access. It's more type-safe than raw React Native native modules, with a cleaner async callback design — and it's approachable even if you're new to native development.
② Expo SDK Standard Libraries (Quickest)
Packages like expo-camera, expo-health, and expo-nfc already cover most native use cases out of the box. Start here first, and only reach for custom modules when the SDK falls short.
③ Raw React Native Bridge (Advanced) The last resort for APIs that don't exist in the SDK or Expo Modules. This sometimes means diving into Objective-C or C++, and the maintenance burden is high. Keep its footprint in production apps as small as possible.
TypeScript ↔ Native Type Mapping
Type mismatches between TypeScript and Swift/Kotlin are one of the most common sources of native integration bugs. The two languages handle null very differently, and numeric precision differences can cause subtle data corruption.
// TypeScript-side type definition for Expo Modules
export interface HealthDataRecord {
timestamp: number; // Unix ms — requires conversion from Swift's TimeInterval
heartRate: number; // Swift Double maps cleanly to number
steps: number; // Int64 → number can lose precision for very large values
unit: string; // Convert Swift's HKUnit.description to string
}// Swift: corresponding native implementation
@objc(HealthKitModule)
class HealthKitModule: Module {
func definition() -> ModuleDefinition {
AsyncFunction("getHeartRate") { (startMs: Double, endMs: Double) -> [String: Any] in
let start = Date(timeIntervalSince1970: startMs / 1000)
let end = Date(timeIntervalSince1970: endMs / 1000)
// ... HealthKit query
return ["timestamp": end.timeIntervalSince1970 * 1000,
"heartRate": latestValue,
"unit": "bpm"]
}
}
}HealthKit Integration (iOS)
Healthcare is one of the highest-monetization categories on the App Store. With a solid HealthKit integration, you can ship fitness trackers, clinical dashboards, and wellness apps with subscription pricing that's hard to justify with standard data sources.
Permission Timing Drives Revenue
HealthKit permission requests must happen at the exact moment a user understands why access is needed. Asking on launch — before they've seen anything useful — results in denials you can never recover from. The recommended pattern is just-in-time permission requests.
import * as Health from 'expo-health';
import { Alert } from 'react-native';
export async function requestHealthPermission(): Promise<boolean> {
const status = await Health.getPermissionStatusAsync([
Health.HealthDataType.HEART_RATE,
Health.HealthDataType.STEP_COUNT,
Health.HealthDataType.SLEEP_ANALYSIS,
]);
if (status === Health.PermissionStatus.GRANTED) return true;
return new Promise((resolve) => {
Alert.alert(
'Access Your Health Data',
'We use heart rate, step count, and sleep data to generate your personalized health report.',
[
{ text: 'Not Now', onPress: () => resolve(false) },
{
text: 'Allow Access',
onPress: async () => {
const result = await Health.requestPermissionsAsync([
Health.HealthDataType.HEART_RATE,
Health.HealthDataType.STEP_COUNT,
]);
resolve(result.granted);
},
},
]
);
});
}Background Sync and Battery Efficiency
To receive real-time HealthKit updates, you'll need enableBackgroundDelivery. Match the update frequency to your app's actual needs — over-fetching drains the battery and users will notice.
let heartRateType = HKObjectType.quantityType(forIdentifier: .heartRate)!
healthStore.enableBackgroundDelivery(
for: heartRateType,
frequency: .hourly, // .immediate for wearable apps; .hourly for most others
withCompletion: { success, error in
if let error = error {
print("Background delivery error: \(error.localizedDescription)")
}
}
)Use .immediate only for apps paired with real-time wearables. Daily report apps can use .daily. For most use cases, .hourly strikes the right balance between freshness and battery impact.
CameraX Integration (Android)
CameraX dramatically simplifies Android camera code compared to Camera2, and it integrates cleanly with Rork Max's Expo module layer.
Real-Time OCR with MLKit
Combining CameraX with Google's MLKit for on-device text recognition opens up high-value business use cases: business card scanning, receipt parsing, QR code reading, and more.
@ExpoMethod
fun startTextRecognition(promise: Promise) {
val recognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS)
val imageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
imageAnalysis.setAnalyzer(ContextCompat.getMainExecutor(context)) { imageProxy ->
val mediaImage = imageProxy.image ?: run {
imageProxy.close()
return@setAnalyzer
}
val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
recognizer.process(inputImage)
.addOnSuccessListener { visionText ->
val result = visionText.textBlocks.joinToString("\n") { it.text }
promise.resolve(result)
}
.addOnFailureListener { e ->
promise.reject("OCR_ERROR", e.message, e)
}
.addOnCompleteListener {
imageProxy.close() // Forgetting this freezes the camera preview
}
}
}Failing to call imageProxy.close() is the single most common cause of frozen camera previews. Always close it inside addOnCompleteListener, not just in the success or failure callbacks.
Handling Device Fragmentation
Android devices vary enormously in camera capability. Settings tuned for a Pixel flagship will cause OOM crashes or laggy previews on mid-range hardware.
fun getOptimalCameraConfig(): CameraConfig {
val availableMemoryMB = getAvailableMemoryMB()
val cameraCapabilities = CameraInfo.getCameraCharacteristics()
return when {
availableMemoryMB > 512 && cameraCapabilities.supportsRaw ->
CameraConfig(resolution = Size(4032, 3024), enableHDR = true)
availableMemoryMB > 256 ->
CameraConfig(resolution = Size(1920, 1080), enableHDR = false)
else ->
CameraConfig(resolution = Size(1280, 720), enableHDR = false)
}
}AR Integration (ARKit / ARCore)
Augmented reality apps are uniquely easy to showcase with demo videos, which helps enormously with App Store and Google Play featuring. Furniture placement simulators and virtual try-on apps consistently show strong purchase conversion, making them attractive for D2C brand partnerships.
Plane Detection and Object Placement
The most fundamental AR interaction — detecting a horizontal surface and placing a 3D object on it — looks like this in Rork Max:
import { ARView, PlaneDetection } from 'expo-ar';
export function ARFurniturePlacement() {
const [planes, setPlanes] = useState<ARPlane[]>([]);
const [placedObjects, setPlacedObjects] = useState<PlacedObject[]>([]);
const handlePlaneDetected = useCallback((newPlanes: ARPlane[]) => {
setPlanes(prev => [...prev, ...newPlanes]);
}, []);
const handleTap = useCallback((event: ARTapEvent) => {
const hitResult = event.hitResults.find(r => r.type === 'existingPlane');
if (!hitResult) return;
setPlacedObjects(prev => [
...prev,
{
id: Date.now().toString(),
modelUri: selectedFurnitureModel,
transform: hitResult.worldTransform,
},
]);
}, [selectedFurnitureModel]);
return (
<ARView
planeDetection={PlaneDetection.Horizontal}
onPlanesDetected={handlePlaneDetected}
onTap={handleTap}
>
{placedObjects.map(obj => (
<ARModel key={obj.id} uri={obj.modelUri} transform={obj.transform} />
))}
</ARView>
);
}Bridging ARKit and ARCore Differences
A shared utility layer that normalizes platform differences keeps your app logic clean and prevents subtle platform-specific bugs from creeping in.
import { Platform } from 'react-native';
export const ARUtils = {
// ARKit reports plane sizes in cm; ARCore uses meters
normalizeSize: (rawSize: number): number =>
Platform.OS === 'ios' ? rawSize / 100 : rawSize,
// Tracking quality thresholds differ between frameworks
isTrackingReliable: (quality: number): boolean =>
Platform.OS === 'ios' ? quality > 0.7 : quality > 0.5,
// Ambient light estimation uses different scales
getLightEstimate: (frame: ARFrame): LightEstimate => {
if (Platform.OS === 'ios') {
return { intensity: frame.lightEstimate?.ambientIntensity ?? 1000 };
} else {
return { intensity: (frame.lightEstimate?.pixelIntensity ?? 1.0) * 1000 };
}
},
};NFC Payments and Tag Reading
NFC creates powerful offline-to-online bridge scenarios: loyalty cards, venue check-ins, ticket validation, and point-of-sale supplements. If your app connects physical retail to a digital experience, NFC is worth the implementation effort.
Understanding iOS vs. Android NFC Limitations
iOS NFC access is heavily restricted — it's effectively read-only for third-party apps. Android supports both reading and writing. Design around this constraint from the beginning, or you'll face painful rewrites later.
import * as NFC from 'expo-nfc';
import { Platform, Alert } from 'react-native';
export async function readNFCTag(): Promise<NFCTagData | null> {
const isSupported = await NFC.isNFCSupportedAsync();
if (!isSupported) {
Alert.alert('Not Supported', 'Your device does not support NFC.');
return null;
}
try {
const tag = await NFC.readTagAsync({
alertMessage: 'Hold your device near the NFC tag', // iOS only
timeout: 10000,
});
const payload = tag.ndefRecords?.[0]?.payload;
if (!payload) return null;
// UTF-8 Text Records have a 3-byte language header to skip
const text = new TextDecoder().decode(payload.slice(3));
return { rawId: tag.id, text };
} catch (error: unknown) {
const e = error as { code?: string };
if (e.code === 'USER_CANCELLED') return null;
if (e.code === 'TIMEOUT') {
Alert.alert('Timed Out', 'No NFC tag was detected. Please try again.');
return null;
}
throw error;
}
}Permission Management Best Practices
Most native features require OS-level permissions. Poor permission design is one of the leading causes of low App Store ratings — users don't mind granting permissions, but they do mind being asked at the wrong time for the wrong reasons.
Progressive Permission Requests
Never request all permissions upfront. The "just-in-time with context" pattern consistently outperforms upfront requests in both grant rates and user satisfaction.
import * as Permissions from 'expo-permissions';
type PermissionContext = {
feature: string;
reason: string;
permission: Permissions.PermissionType;
};
export async function requestPermissionWithContext(
ctx: PermissionContext
): Promise<boolean> {
const { status } = await Permissions.getAsync(ctx.permission);
if (status === 'granted') return true;
if (status === 'denied') {
// Once denied, the only path is through Settings
Alert.alert(
`Enable ${ctx.feature}`,
`${ctx.reason}\nPlease enable "${ctx.feature}" in your device Settings.`,
[
{ text: 'Cancel' },
{ text: 'Open Settings', onPress: () => Linking.openSettings() },
]
);
return false;
}
// First-time request with explanation
const { status: newStatus } = await Permissions.askAsync(ctx.permission);
return newStatus === 'granted';
}Memory Leak Prevention and Production Stability
The two most common causes of production crashes in React Native + native module apps are forgotten event listener cleanup and holding large binary data in memory longer than necessary.
Always Clean Up in useEffect
export function HealthMonitor() {
const subscriptionRef = useRef<HealthSubscription | null>(null);
useEffect(() => {
let isMounted = true;
async function subscribe() {
subscriptionRef.current = await Health.watchStepCount(data => {
if (!isMounted) return; // Prevent setState after unmount
setSteps(data.steps);
});
}
subscribe();
return () => {
isMounted = false;
subscriptionRef.current?.remove(); // This is the leak if you forget it
subscriptionRef.current = null;
};
}, []);
}Release Large Images and Video Promptly
Keeping camera or AR frames in memory beyond the processing window is a reliable path to OOM crashes.
import * as FileSystem from 'expo-file-system';
export async function processAndReleaseImage(uri: string): Promise<ProcessedResult> {
try {
const result = await analyzeImage(uri);
return result;
} finally {
// finally ensures cleanup even if analysis throws
const fileInfo = await FileSystem.getInfoAsync(uri);
if (fileInfo.exists) {
await FileSystem.deleteAsync(uri, { idempotent: true });
}
}
}Responding to Memory Pressure
import { AppState, Platform } from 'react-native';
export function useMemoryPressureHandler(clearCache: () => void) {
useEffect(() => {
if (Platform.OS === 'ios') {
// iOS fires a dedicated memoryWarning event
const handler = AppState.addEventListener('memoryWarning' as any, clearCache);
return () => handler.remove();
} else {
// Android: clear cache when the app moves to background
const handler = AppState.addEventListener('change', (state) => {
if (state === 'background') clearCache();
});
return () => handler.remove();
}
}, [clearCache]);
}Platform-Specific Testing Strategy
Unit tests alone are insufficient for native integrations. The bugs that matter most only show up on real devices, under real conditions.
iOS Testing Checklist
Verify that permission dialogs appear at the right moment — not on launch. Then disable each permission in Settings, return to the app, and confirm the fallback UI behaves correctly. Test background-to-foreground transitions for AR and HealthKit sessions: they should pause gracefully and resume without errors. Finally, use Xcode's "Simulate Memory Warning" to confirm your app releases caches without crashing.
Android Testing Checklist
Run your app on devices with multiple API levels — file access permission behavior changed significantly in Android 10 (API 29). Test Doze mode and battery optimization with adb shell dumpsys deviceidle force-idle to confirm background tasks behave as expected. Pay attention to manufacturer-specific camera behavior: Samsung, Pixel, and Xiaomi devices can differ in frame rate, color correction, and HDR handling.
Mocking Native Modules in Jest
// __mocks__/expo-health.ts
const mockHealthData = {
steps: 8432,
heartRate: 72,
timestamp: Date.now(),
};
export const getPermissionStatusAsync = jest.fn().mockResolvedValue('granted');
export const requestPermissionsAsync = jest.fn().mockResolvedValue({ granted: true });
export const getStepCountAsync = jest.fn().mockResolvedValue(mockHealthData.steps);
export const getHeartRateAsync = jest.fn().mockResolvedValue(mockHealthData.heartRate);
export const watchStepCount = jest.fn().mockReturnValue({ remove: jest.fn() });
// Example test
describe('HealthMonitor', () => {
it('shows fallback UI when permission is denied', async () => {
getPermissionStatusAsync.mockResolvedValueOnce('denied');
const { getByText } = render(<HealthMonitor />);
await waitFor(() => {
expect(getByText('Please enable access in Settings')).toBeTruthy();
});
});
});Wrapping Up
Mastering native integration in Rork Max opens the door to genuinely high-value products:
- Healthcare: HealthKit-powered dashboards for clinicians or patients ($30–$99/month subscriptions)
- AR/Visual: ARKit/ARCore furniture placement simulators ($9.99 one-time)
- Payments: NFC-based loyalty card apps for physical retail (3% transaction fee)
Three core skills to take away from this guide:
- Type-safe Bridge design — get TypeScript and Swift/Kotlin types to agree upfront
- Understand platform differences — HealthKit and Google Fit are not the same thing
- Own your production stability — monitor memory, battery, and crash logs like a professional
Next steps:
- How to Publish Your Rork App to the App Store and Google Play
- Rork App Performance Optimization — Rendering, Memory, and Networking
- Implementing In-App Purchases and Subscriptions with RevenueCat in Rork