Expo Modules API v2 Native Extensions
Swift/Kotlin modules, inline modules, type generation, and testing strategy.
Key takeaways
- SDK 56 adds a faster path from app-local native experiments to reusable modules, including experimental inline Kotlin/Swift files that autolink from app directories.
expo-type-informationgenerates TypeScript interfaces from Swift modules, and a Kotlin compiler plugin lowers Android reflection overhead.- Choose inline modules for one-app capabilities and local or standalone Expo modules once the capability is reused across apps.
- Enable inline modules via
experiments.inlineModules.watchedDirectories, then regenerate native projects withnpx expo prebuild. - Keep a typed
requireNativeModulewrapper instead of leakingany, and treat native runtime changes as binary releases, not OTA-only changes.
What Changed in SDK 56
Expo Modules now support a faster path from app-local native experiments to reusable modules. Teams can start with inline native files and promote them into local or standalone modules when reuse, versioning, or ownership requires it.
| Feature | Status | Production meaning |
|---|---|---|
| Inline modules | SDK 56 experimental | Kotlin and Swift files can live in app directories and be autolinked |
expo-type-information | SDK 56 | Generate TypeScript interfaces from Swift modules |
create-expo-module improvements | SDK 56 | Better local/standalone scaffolding and non-interactive flows |
| Kotlin compiler plugin | SDK 56 | Lower reflection overhead for Android Expo Modules |
| iOS JSI layer improvements | SDK 56 | Simpler Swift-to-JSI call path |
Selection Guide
| Situation | Recommendation |
|---|---|
| Thin native capability used by one app | Inline module |
| Capability reused across apps | local or standalone Expo module |
| Vendor Android/iOS SDK wrapper | Expo Modules API wrapper |
| Screen itself must be native UI | Expo UI custom view or modifier first |
| Existing host native app | expo-brownfield plus host Turbo Module registration |
Inline Module Baseline
{
"expo": {
"experiments": {
"inlineModules": {
"watchedDirectories": ["app"]
}
}
}
}After changing this setting, regenerate native projects with npx expo prebuild.
package app
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
class DevicePolicyModule : Module() {
override fun definition() = ModuleDefinition {
Name("DevicePolicy")
AsyncFunction("isManagedDevice") {
false
}
}
}internal import ExpoModulesCore
public class DevicePolicyModule: Module {
public func definition() -> ModuleDefinition {
Name("DevicePolicy")
AsyncFunction("isManagedDevice") {
return false
}
}
}TypeScript Contract
Native modules become an operations risk when the JS side leaks any. Use SDK 56 type generation
where possible, or keep a stable wrapper interface.
import { requireNativeModule } from 'expo';
type DevicePolicyModule = {
isManagedDevice(): Promise<boolean>;
};
export default requireNativeModule<DevicePolicyModule>('DevicePolicy');Testing Strategy
- Test public native module APIs through a TypeScript contract test.
- Use JUnit/Robolectric on Android and XCTest on iOS for permission and error branches.
- Mock native-unavailable states in the JS wrapper.
- Use EAS preview builds to verify signing, entitlements, and permission prompts.
- Mark native runtime changes separately from OTA-only changes in the PR template.
Operating Rules
- Native capability changes can require a new
runtimeVersionand binary release. - Do not place secrets or tenant configuration in native source.
- Promote inline modules into local Expo modules when they are duplicated across apps.
- If the module may be published, manage semver, changelog, and config plugin migrations together.