Expo Modules API u 2026: vlastiti native modul u Swiftu i Kotlinu
Naučite kako napisati vlastite Expo native module u Swiftu i Kotlinu za React Native 2026 s primjerima za records, eventove, native views i config plugins.
Expo Modules API je Expov način pisanja vlastitih native modula u Swiftu i Kotlinu s minimalnim boilerplateom, punom podrškom za New Architecture i bridgeless mode te automatskom kompatibilnošću sa starom arhitekturom. Ako radiš na Expo ili bare React Native projektu u 2026. i trebaš dodati funkcionalnost koje nema u postojećim paketima (pristup HealthKitu, custom kameru, hardversku enkripciju), Expo Modules API je pragmatičan izbor koji ti štedi tjedne posla u odnosu na ručno pisanje Turbo Modula.
Expo Modules API u 2026. dolazi s prvorazrednom potporom za New Architecture. Moduli automatski rade u bridgeless modeu bez dodatne konfiguracije.
Local modules (unutar modules/ direktorija) i novi inline modules (Swift/Kotlin uz app kod) omogućuju iteraciju bez publish ciklusa na npm.
Records su konvertibilni tipovi koji daju compile-time tipsku sigurnost između JS-a i Swifta/Kotlina bez ručne validacije.
Config plugins su obavezni ako mijenjaš Info.plist, AndroidManifest.xml ili Podfile. Bez njih EAS Build ne zna kako regenerirati native projekte.
Turbo Modules biraj samo ako ti treba C++ interoperabilnost ili radiš na core-u React Nativea; za sve ostalo Expo Modules ima kraći DX.
Što je Expo Modules API i zašto ga koristiti
Expo Modules API je deklarativni DSL napisan u Swiftu (iOS) i Kotlinu (Android) koji ti dopušta da definiraš funkcije, konstante, view componente i eventove koje želiš izložiti JavaScriptu. Umjesto da ručno pišeš RCT_EXPORT_METHOD makroe, JNI potpise ili Turbo Module code-gen sheme, opišeš modul u ModuleDefinition bloku i framework napravi ostalo za tebe. Honestly, u šest React Native aplikacija koje sam otpremio, samo dvije su zahtijevale ručno napisan Turbo Module. Sve ostalo je pokrio Expo Modules API u dijelu vremena.
Ono što ga čini posebno vrijednim u 2026. je automatska podrška za New Architecture. Modul napisan po Expo Modules API-ju radi u bridgeless modeu bez dodatne konfiguracije, ali ostaje kompatibilan sa starom arhitekturom za projekte koji još nisu migrirali. Ako te zanima šira slika, pročitaj naš vodič o novoj arhitekturi React Nativea kroz JSI, Fabric i Bridgeless Mode. Expo Modules API sjeda točno na taj sloj.
Pragmatično, koristiš ga u tri glavna scenarija: (1) trebaš platform API koji nije pokriven Expo SDK-om (npr. NEHotspotConfiguration na iOS-u), (2) wrapaš postojeću native SDK biblioteku (Stripe, Braze, HealthKit) za React Native, (3) izvodiš performance-critical logiku iz JS-a u native (kriptografija, image processing, tight loops). Za sve ostalo, pogotovo za stvari koje već imaju community wrapper, nemoj pisati vlastiti modul. Održavanje košta.
Expo Modules API vs Turbo Modules: kako birati
Pitanje "Expo Modules ili Turbo Modules" je 2026. najčešća dilema na mojim code reviewima. Odgovor je kratak: ako ne pišeš C++ i ne smeta ti dependency na expo paket, uzmi Expo Modules API. Preporuka dolazi i s službene React Native dokumentacije za Turbo Modules. Turbo Modules su primarno namijenjeni ljudima koji rade na infrastrukturnoj razini React Nativea ili trebaju direktan pristup JSI i C++ layer-u.
Dimenzija
Expo Modules API
Turbo Modules
Jezici
Swift, Kotlin (native only)
Obj-C++, C++, Java/Kotlin
Boilerplate
Minimalan (DSL u jednom fileu)
Codegen shema + native impl + spec
New Architecture
Automatska iz kutije
Zahtijeva codegen konfiguraciju
Kompatibilnost sa starom arhitekturom
Automatska
Ručna preko interop layera
C++ pristup
Ne (koristi JSI wrappere)
Da, puni JSI pristup
Dependency
Paket expo
Samo react-native
Config plugin integracija
First-class
Ručno
Krivulja učenja
Blaga (nekoliko sati)
Strma (dani do tjedana)
Realan primjer iz prakse. Prošli kvartal sam napisao modul za pristup DeviceCheck API-ju na iOS-u i Play Integrity API-ju na Androidu za anti-fraud. U Expo Modules API-ju to je bilo oko 200 linija Swift/Kotlin koda; u Turbo Modulima procijenjeno je oko 500 linija s codegen shemama. Za CI/CD pipeline i EAS Build integraciju razlika je bila još veća. Expo Modules "just work" s expo prebuild, dok Turbo Modules zahtijevaju dodatne korake.
Kreiranje local modula u minutu
Najbrži put od nule do funkcionalnog modula je local module. To je Swift i Kotlin kod koji živi unutar modules/ direktorija tvoje aplikacije i automatski se povezuje bez publish-a na npm. Idealno za prototipiranje ili za module koji su previše app-specific da bi bili standalone paket.
Za standalone modul koji ćeš objaviti na npm i koristiti u više projekata, izostavi --local zastavicu. Struktura je ista, ali dobiješ i example/ Expo app za razvoj i testiranje. Detalje o tome kako postaviti EAS pipeline za module s native kodom pokriva naš vodič o EAS Build i Submit deploy tijeku.
Novost u 2026. su inline modules. Možeš pisati Swift/Kotlin file direktno pored svog app/ koda, bez potrebe za create-expo-module scaffoldingom. Za male, jednokratne native pozive to je najmanji trošak; framework ih otkrije preko file-based konvencije.
Pisanje Swift modula za iOS
Otvori modules/expo-settings/ios/ExpoSettingsModule.swift. Ovako izgleda pun primjer modula koji perzistira temu (light/dark) u UserDefaults i šalje event kada se promijeni:
import ExpoModulesCore
public class ExpoSettingsModule: Module {
public func definition() -> ModuleDefinition {
Name("ExpoSettings")
// Konstante dostupne iz JS-a kao ExpoSettings.PI
Constants([
"PI": Double.pi
])
// Sync funkcija: poziv blokira JS thread
Function("getTheme") { () -> String in
return UserDefaults.standard.string(forKey: "theme") ?? "system"
}
// Async funkcija: vraća Promise u JS-u
AsyncFunction("setTheme") { (theme: String) in
UserDefaults.standard.set(theme, forKey: "theme")
self.sendEvent("onChangeTheme", [
"theme": theme
])
}
// Deklaracija eventova koje modul emitira
Events("onChangeTheme")
}
}
Nekoliko važnih detalja. Function je sinkrona i vraća vrijednost direktno; AsyncFunction automatski wrappa poziv u JavaScript Promise i može bacati errore koji se propagiraju kao odbijeni promisi. Signature closurea diktira JS API, a parametri i return tip se automatski konvertiraju preko JSI bindingsa bez potrebe za ručnim marshallingom.
Da bi radio testove na uređaju, otvori ios direktorij kroz Xcode s xed ios, pa pod Pods → Development Pods → ExpoSettings pronađeš Swift file za debugiranje s breakpointima. Simulator loguje print izjave direktno u Metro terminal, što je korisno za brzu iteraciju bez Xcode-a.
Pisanje Kotlin modula za Android
Android pandan istog modula živi u modules/expo-settings/android/src/main/java/expo/modules/settings/ExpoSettingsModule.kt. API je namjerno isti kao Swift verzija:
package expo.modules.settings
import android.content.Context
import androidx.core.os.bundleOf
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
class ExpoSettingsModule : Module() {
override fun definition() = ModuleDefinition {
Name("ExpoSettings")
Constants(
"PI" to Math.PI
)
Function("getTheme") {
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
prefs.getString("theme", "system") ?: "system"
}
AsyncFunction("setTheme") { theme: String ->
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
prefs.edit().putString("theme", theme).apply()
sendEvent("onChangeTheme", bundleOf("theme" to theme))
}
Events("onChangeTheme")
}
private val context: Context
get() = requireNotNull(appContext.reactContext)
}
Payload eventova na Androidu je Bundle, pa koristi bundleOf helper iz androidx.core za konciznu inicijalizaciju. Ako paziš na consistency: JS API je identičan iOS verziji, što znači da tvoj index.ts ne treba znati za platform-specific detalje.
Za pristup Android Context-u koristi appContext.reactContext preko helper property-ja kao gore. Ne cacheiraj Context u instance polja, jer bi activity recreacije ostavile dangling reference. Isti princip vrijedi i za activities: dohvati appContext.currentActivity tek kad ti stvarno treba, ne u init bloku.
Records i tipska sigurnost preko mosta
Records su konvertibilni tipovi koje Expo Modules API koristi za slanje strukturiranih objekata između JS-a i native koda. Umjesto da primaš Dictionary<String, Any> i ručno validiraš polja, definiraš struct s tipovima i default vrijednostima, a framework napravi validaciju i konverziju automatski.
Swift primjer koji prima korisničku konfiguraciju:
struct AppearanceOptions: Record {
@Field var theme: String = "system"
@Field var accentColor: String? = nil
@Field var reducedMotion: Bool = false
}
AsyncFunction("configure") { (options: AppearanceOptions) in
// options.theme, options.accentColor, options.reducedMotion su tipski sigurni
apply(options)
}
Kotlin ekvivalent koristi klasu s Field anotacijama:
class AppearanceOptions : Record {
@Field var theme: String = "system"
@Field var accentColor: String? = null
@Field var reducedMotion: Boolean = false
}
Iz JS-a je poziv trivialan: ExpoSettings.configure({ theme: "dark", reducedMotion: true }). Ako pošalješ krivi tip (recimo reducedMotion: "yes" umjesto boolean-a), modul baci opisnu grešku prije nego kod dođe do tvog closurea. To spašava sate debugiranja u odnosu na "silent coerce" ponašanje starog mosta. Ja sam ovaj problem osobno pogodio u prošlom projektu s HealthKit modulom, i migracija na Records je sama otklonila cijelu klasu bugova.
Events: dvosmjerna komunikacija JS ↔ native
Za push podataka iz native koda u JS koristi eventove. Deklariraj ih u ModuleDefinition s Events("onSomething"), pa ih emitraj s sendEvent pozivom. Sa JS strane, wrap-aj ih u EventEmitter koji Expo Modules pruža:
// modules/expo-settings/src/index.ts
import { EventEmitter, NativeModulesProxy } from "expo-modules-core";
import ExpoSettings from "./ExpoSettingsModule";
const emitter = new EventEmitter(ExpoSettings);
export function addThemeListener(
listener: (event: { theme: string }) => void
) {
return emitter.addListener("onChangeTheme", listener);
}
export function setTheme(theme: "light" | "dark" | "system") {
return ExpoSettings.setTheme(theme);
}
Watchout: emiter je singleton per module. Ako radiš više addListener poziva u istoj sesiji, ne zaboravi remove() u cleanup funkciji. Curenje listenera je najčešći uzrok "phantom" event handlera nakon fast refresha. Meni je taj bug jednom pojeo pola dana prije nego sam ga uhvatio u profileru.
Native views: kako izložiti UIView i View
Osim funkcija, možeš izložiti i native view komponente: na iOS-u UIView, a na Androidu View podklasu. Definicija view-a ide u zaseban file (npr. ExpoSettingsView.swift) i registrira se u glavnoj ModuleDefinition preko View bloka:
Sa JS strane dobiješ standardnu React komponentu koju uvezeš i koristiš kao bilo koji drugi View. Fabric renderer je zadužen za mount i measure, tako da ne moraš pisati shadow node code ručno. Ovaj pristup je pobjeda ako radiš embed native SDK komponenti (Stripe PaymentSheet, MapKit, Google Pay tipke).
Config plugins i CNG workflow
Ako tvoj modul zahtijeva izmjene u Info.plist, AndroidManifest.xml, Podfile ili build gradleu, obavezno napiši config plugin. Bez njega Expo prebuild i EAS Build će regenerirati native projekte i tvoje ručne izmjene će nestati. Ovo je najčešća greška koju vidim na code reviewima kada developeri prvi put pišu Expo module.
// modules/expo-settings/plugin/withSettings.ts
import { ConfigPlugin, withInfoPlist } from "expo/config-plugins";
const withSettings: ConfigPlugin = (config) => {
return withInfoPlist(config, (config) => {
config.modResults.NSFaceIDUsageDescription =
"Trebamo Face ID za sigurnu autentifikaciju.";
return config;
});
};
export default withSettings;
Zatim referenciraš plugin iz app.json:
{
"expo": {
"plugins": ["expo-settings"]
}
}
Pogledaj službeni tutorial za config plugin i native modul za kompletan primjer koji dodaje app extension i modificira Podfile. Za advanced modove (dodavanje entitlementsa, background modova, custom URL scheme handling), @expo/config-plugins paket ima helper funkcije za sve česte slučajeve.
Testiranje modula i publish na npm
Ako si generirao standalone modul (bez --local zastavice), CLI ti je već napravio example/ Expo app unutar modula. To je tvoja razvojna aplikacija, pa je pokreni s npm run open:ios ili npm run open:android iz root direktorija modula. Metro će watchati Swift i Kotlin fileove i rebuildati aplikaciju automatski kad ih promijeniš.
Za unit testove native koda koristi Xcode XCTest targete i Android JUnit testove. Expo Modules API ne ograničava standardne native test alate. Za integration testove koji verificiraju JS ↔ native boundary preferiram Maestro flow-ove; više detalja o E2E setupu je u Expo Module API referenci.
Publish je standardni npm workflow: npm version patch, pa npm publish. Prije publisha provjeri da expo-module.config.json ima točan platforms array i da package.json uključuje ios/, android/ i src/ direktorije u files polju. Bez toga korisnici tvog paketa neće dobiti native izvorni kod nakon npm install.
Česta pitanja
Radi li Expo Modules API u bare React Native projektu bez Expo SDK-a?
Da, ali moraš instalirati expo paket i pokrenuti npx install-expo-modules koji doda potrebnu integraciju u iOS i Android projekte. Nakon toga, bare RN projekt može koristiti bilo koji Expo module kao regularni npm paket.
Mogu li koristiti C++ kod u Expo Modules API-ju?
Ne direktno kao u Turbo Modules, jer je Expo Modules API namijenjen Swiftu i Kotlinu. Ako trebaš C++, možeš ga uključiti kao dio native izvornog koda i pozvati ga iz Swifta preko bridging headera ili iz Kotlina preko JNI. Za punopravni JSI ↔ C++ pipeline Turbo Modules su primjereniji.
Trebam li migraciju modula za React Native 0.76+ i bridgeless mode?
Ne trebaš. Moduli napisani po Expo Modules API-ju automatski rade i u bridgeless modeu i u legacy bridge modeu. Framework interno drži oba adaptera. Ovo je jedan od glavnih razloga zašto ga preporučam preko ručnog pisanja Turbo Modula za većinu use-caseova.
Kako debugirati Swift i Kotlin kod unutar Expo modula?
Za iOS otvori ios direktorij s xed ios i debugiraj kroz Xcode s breakpointima pod Pods → Development Pods → <ModuleName>. Za Android otvori android direktorij u Android Studiju kao Gradle projekt i attachaj debugger na Metro proces. Log izjave (Swift print, Kotlin Log.d) pojavljuju se u Metro terminalu.
Koja je razlika između local i inline modula?
Local moduli žive u modules/ direktoriju s vlastitim package.json-om i expo-module.config.json-om, i pogodni su za srednje složene module koji imaju više fileova. Inline moduli su Swift/Kotlin fileovi direktno pored app/ koda, bez scaffoldinga, i pogodni su za mali, jednokratni native poziv koji ne opravdava zaseban paket.
Praktičan vodič za forme u React Nativeu 2026: kombinacija React Hook Form 7.80, Zod 4 sheme i Controller uzorka za tipovima sigurne obrasce, minimalne re-rendere i validaciju koja se dijeli s backendom.
Naučite slojeviti pristup testiranju React Native aplikacija u 2026. Jest za unit testove, RNTL za komponente, Maestro i Detox za E2E — s praktičnim primjerima koda i GitHub Actions CI/CD integracijom.