Expo Modules API 2026: Typovo bezpečné natívne moduly v Swifte a Kotline

Sprievodca Expo Modules API v roku 2026: od create-expo-module scaffoldu cez Function/AsyncFunction DSL, Records a Convertibles až po natívne View, config plugin a upgrade playbook pre platform tímy.

Expo Modules API 2026: Swift & Kotlin

Aktualizované: 17. júla 2026

Expo Modules API je moderné rozhranie pre písanie natívnych modulov v Swifte a Kotline, ktoré nahrádza staré ObjC/Java bridge s type-safe DSL, automatickými konverziami argumentov a plnou podporou React Native New Architecture. V roku 2026, po rokoch v produkčnom fintech kóde, je to moja predvolená voľba všade tam, kde nepotrebujem C++. Tento sprievodca ukáže, ako vytvoriť lokálny aj distribučný modul, ako pracovať s Function, AsyncFunction, Record, natívnymi View, eventmi a config plugin-om. Všetko s dôrazom na typovú bezpečnosť a čistotu monorepa.

  • Expo Modules API generuje bindings zo Swift/Kotlin DSL, takže most (bridge) medzi JS a natívom je celý typovo pokrytý bez ručného JSI/Turbo boilerplate.
  • create-expo-module scaffolduje monorepo-friendly package so Swift/Kotlin/TS zdrojmi, example appkou a expo-module.config.json, ideálne pre platform tímy.
  • Record, Enumerable a Convertible protokoly zaručujú, že neplatné JS objekty spadnú s presnou chybou skôr, než sa vôbec dostanú do natívneho kódu.
  • Expo Modules v roku 2026 bežia priamo cez JSI bez bridge módu a sú plne kompatibilné s Fabric a Turbo Modules na New Architecture.
  • Ak nepíšete C++ interop, Expo Modules ušetria 60 až 70 % boilerplate oproti čistým Turbo Modules pri porovnateľnej priepustnosti (stovky tisíc volaní/s).
  • Config plugin umožňuje deklaratívne upravovať Info.plist, AndroidManifest a Gradle bez opustenia Continuous Native Generation workflowu.

Čo je Expo Modules API a prečo ho v roku 2026 preferovať

Expo Modules API je set knižníc, DSL a code-generátorov, ktoré vám umožňujú písať natívny iOS kód v Swifte a natívny Android kód v Kotline, pričom o most, konverzie typov a registráciu v JS runtime sa stará framework. Od chvíle, keď sa bridgeless mode stal defaultom pre React Native 0.79+, dostal moderný Expo Modules ekosystém definitívnu formu. Každý modul beží cez JSI, každá funkcia má explicitnú signatúru a JS strana dostane presne typované volania. V mojom fintech tíme sme za posledný rok migrovali 12 zo 14 interných natívnych balíkov na Expo Modules a počet NSInvalidArgumentException incidentov v Sentrym klesol o 82 %.

Historicky sme písali natívne moduly cez RCTBridgeModule (ObjC) alebo ReactMethod (Java). Chýbala kompilátorová kontrola typov, cez JSON sa serializovalo všetko a null-safety bola len na papieri. Turbo Modules situáciu čiastočne zlepšili cez codegen, ale stále musíte písať flow-typed .js spec, generovať wrappery a manuálne implementovať C++ interfacy.

Expo Modules API to celé skryje. Napíšete Swift Module { … } blok, deklarujete Function("hash") { (input: String) -> String in … } a JS bindingy sa vygenerujú počas buildu. Pre platform tím, ktorý spravuje monorepo s viacerými apkami, je to obrovský posun v konzistencii. Honestly, prvýkrát som tomu neveril, kým som si to nevyskúšal na dvoch reálnych moduloch.

Expo Modules vs Turbo Modules: ktorý si vybrať

Otázka „Expo Modules alebo Turbo Modules?" sa v issue trackeroch objavuje týždenne. Krátka odpoveď: ak nepíšete C++ interop alebo neintegrujete existujúcu C/C++ knižnicu s vlastnou JSI vrstvou, siahnite po Expo Modules. Ak dopĺňate React Native Renderer alebo potrebujete zdieľať kód s natívnym iOS/Android tímom v C++, siahnite po Turbo Modules. Nižšie je porovnanie, ktoré používam v mojich architektúrnych review.

VlastnosťExpo Modules APITurbo Modules
Primárne jazykySwift + KotlinC++, ObjC++, Java + codegen spec
BoilerplateNízky (DSL)Vysoký (spec + codegen + implementácia)
Typová kontrolaAutomatická cez konverteryCez ručne písaný Flow/TS spec
C++ interopNepodporovanéPrvotriedne
JSI runtimeÁno (bridgeless)Áno (bridgeless)
Config plugin podporaVstavanáRučne cez expo-config
Priepustnosť volaní/s~350k~500k
Krivka učeniaKrátka pre Swift/Kotlin vývojárovStrmá

V praxi je rozdiel v priepustnosti irelevantný. 99 % modulov nikdy nedosiahne ani 10 000 volaní/s. Rozhodujúce sú maintainability a onboarding time. Náš iOS senior sa naučil Expo Modules DSL za jeden deň; Turbo Modules mu zabrali dva týždne, kým vôbec dokázal pridať novú funkciu do existujúceho modulu. Ak plánujete migráciu na novú architektúru React Native s Fabric a TurboModules, Expo Modules dostanete zadarmo. Sú s ňou plne kompatibilné.

Príprava projektu a scaffolding cez create-expo-module

Predpokladom je Expo SDK 53+, Node 20+ a nainštalovaný Xcode 16 a Android Studio Ladybug Feature Drop. Pre samostatný modul, ktorý budete distribuovať cez npm alebo interné workspace, spustite scaffolding skript v prázdnom priečinku:

npx create-expo-module@latest expo-secure-hash
cd expo-secure-hash
npm run build
npm run open:ios     # otvorí príkladovú app v Xcode
npm run open:android # otvorí príkladovú app v Android Studio

Skript sa vás pýta na názov balíka, autora a repozitár. Vytvorí štruktúru so src/ (TypeScript verejné API), ios/ (Swift), android/ (Kotlin), example/ (spustiteľná testovacia app) a expo-module.config.json. Práve expo-module.config.json je manifest, ktorý povie autolinking mechanizmu, aké platformy a moduly balík obsahuje. Nedotýkajte sa jeho platforms poľa manuálne, pridávajte len cez expo-module add.

Anatómia modulu: Module DSL, Function, AsyncFunction

Srdcom každého Expo modulu je Swift/Kotlin trieda dediaca z Module. Vo vnútri definition() deklarujete verejnú API pomocou DSL. Pozrime sa na iOS implementáciu SHA-256 hasher s podporou soli:

import ExpoModulesCore
import CryptoKit

public class SecureHashModule: Module {
  public func definition() -> ModuleDefinition {
    Name("SecureHash")

    Constants([
      "algorithm": "SHA-256",
      "version": "1.0.0"
    ])

    Function("hash") { (input: String, salt: String?) -> String in
      let payload = (salt ?? "") + input
      let digest = SHA256.hash(data: Data(payload.utf8))
      return digest.map { String(format: "%02x", $0) }.joined()
    }

    AsyncFunction("hashFile") { (uri: URL) async throws -> String in
      let data = try Data(contentsOf: uri)
      let digest = SHA256.hash(data: data)
      return digest.map { String(format: "%02x", $0) }.joined()
    }

    Events("onHashProgress")
  }
}

Ekvivalent v Kotline pre Android vyzerá skoro identicky. DSL je zámerne symetrické:

package expo.modules.securehash

import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import java.net.URL
import java.security.MessageDigest

class SecureHashModule : Module() {
  override fun definition() = ModuleDefinition {
    Name("SecureHash")

    Constants(
      "algorithm" to "SHA-256",
      "version" to "1.0.0"
    )

    Function("hash") { input: String, salt: String? ->
      val payload = (salt ?: "") + input
      MessageDigest.getInstance("SHA-256")
        .digest(payload.toByteArray())
        .joinToString("") { "%02x".format(it) }
    }

    AsyncFunction("hashFile") { uri: URL ->
      val bytes = uri.openStream().use { it.readBytes() }
      MessageDigest.getInstance("SHA-256")
        .digest(bytes)
        .joinToString("") { "%02x".format(it) }
    }

    Events("onHashProgress")
  }
}

Kľúčový rozdiel oproti Turbo Modules: nikde nepíšete @objc, @ReactMethod, JSI wrapper ani JSON codec. Framework analyzuje typy argumentov a návratovej hodnoty a vygeneruje presné bindings. Function je synchrónny (blokuje JS thread na dobu volania), AsyncFunction beží na background thread pool-e a JS strane vráti Promise. Pre I/O operácie, ako je čítanie súboru, vždy siahnite po AsyncFunction. Inak si zamrznete UI, a áno, presne túto chybu som spravil v mojom prvom module.

TypeScript verejné API

Auto-generované bindings sú v SecureHashModule.web.ts a natívne v SecureHashModule.ts. Vlastný verejný wrapper drží typy pre konzumenta:

import { requireNativeModule } from 'expo-modules-core';

interface SecureHashModule {
  algorithm: string;
  version: string;
  hash(input: string, salt?: string | null): string;
  hashFile(uri: string): Promise<string>;
}

export default requireNativeModule<SecureHashModule>('SecureHash');

Typová bezpečnosť: Records, Convertibles a Enums

Ak potrebujete cez most poslať štruktúrovaný objekt, nikdy nepoužívajte [String: Any] alebo Map<String, Any>. Namiesto toho definujte Record. Príklad: modul, ktorý prijme konfiguračný objekt so soľou, algoritmom a časovým limitom:

import ExpoModulesCore

struct HashOptions: Record {
  @Field var algorithm: HashAlgorithm = .sha256
  @Field var salt: String? = nil
  @Field var iterations: Int = 1
  @Field var timeoutMs: Double = 5000
}

enum HashAlgorithm: String, Enumerable {
  case sha256 = "SHA-256"
  case sha512 = "SHA-512"
  case blake3 = "BLAKE3"
}

// v Module { ... }
AsyncFunction("hashWithOptions") { (input: String, options: HashOptions) async throws -> String in
  guard options.iterations > 0 else {
    throw HashError.invalidIterations
  }
  // ... implementácia
}

Keď JS strana zavolá hashWithOptions("payload", { algorithm: "SHA-999" }), framework konverziu odmietne s presnou chybou „Cannot convert 'SHA-999' to enum HashAlgorithm" ešte pred vstupom do vášho kódu. To je typová bezpečnosť, ktorú od bridgu v roku 2026 očakávam. Rovnaká vec v Kotline používa data class a @Field anotácie:

class HashOptions : Record {
  @Field var algorithm: HashAlgorithm = HashAlgorithm.SHA256
  @Field var salt: String? = null
  @Field var iterations: Int = 1
  @Field var timeoutMs: Double = 5000.0
}

enum class HashAlgorithm(val value: String) : Enumerable {
  SHA256("SHA-256"),
  SHA512("SHA-512"),
  BLAKE3("BLAKE3")
}

Pre neštandardné typy (napr. váš vlastný Money value object) implementujte protokol Convertible a definujte statickú metódu convert(from:). Uvidíte, ako to zjednoduší doménovú vrstvu, keď hash a šifrovanie musia rešpektovať menové a decimal-scale pravidlá.

Natívny View v Swifte a Kotline s propmi a eventmi

Expo Modules API podporuje nielen funkcie, ale aj natívne View komponenty. Predpokladajme, že chceme vlastný podpisový panel (canvas), ktorý na iOS používa PencilKit a na Androide android.graphics.Canvas. Deklarácia v Swifte:

import ExpoModulesCore
import PencilKit
import SwiftUI

class SignaturePadView: ExpoView {
  private let canvas = PKCanvasView()
  let onStrokeEnd = EventDispatcher()

  required init(appContext: AppContext? = nil) {
    super.init(appContext: appContext)
    canvas.delegate = self
    canvas.drawingPolicy = .anyInput
    addSubview(canvas)
  }

  override func layoutSubviews() {
    super.layoutSubviews()
    canvas.frame = bounds
  }
}

extension SignaturePadView: PKCanvasViewDelegate {
  func canvasViewDidEndUsingTool(_ canvasView: PKCanvasView) {
    let png = canvasView.drawing.image(from: canvasView.bounds, scale: UIScreen.main.scale)
    onStrokeEnd(["base64": png.pngData()?.base64EncodedString() ?? ""])
  }
}

public class SignaturePadModule: Module {
  public func definition() -> ModuleDefinition {
    Name("SignaturePad")
    View(SignaturePadView.self) {
      Prop("strokeColor") { (view: SignaturePadView, hex: UIColor) in
        view.canvas.tool = PKInkingTool(.pen, color: hex, width: 3)
      }
      Events("onStrokeEnd")
    }
  }
}

Na JS strane komponent použite ako obyčajný React komponent. Framework spraví bindings pre Fabric renderer:

import { requireNativeViewManager } from 'expo-modules-core';
import { ViewProps } from 'react-native';

interface Props extends ViewProps {
  strokeColor?: string;
  onStrokeEnd?: (event: { nativeEvent: { base64: string } }) => void;
}

const NativeView = requireNativeViewManager<Props>('SignaturePad');
export default NativeView;

Ak vás zaujíma, ako sa taký komponent správa pod Fabric a čo urobí Reanimated cez shared values, prečítajte si aj náš rozbor optimalizácie štartu React Native aplikácií v roku 2026. Natívne pohľady majú citeľný vplyv na TTI.

Config plugin pre Info.plist a AndroidManifest

Continuous Native Generation znamená, že ios/ a android/ adresáre sú pri každom expo prebuild zmazané a znovu vygenerované. Ak váš modul potrebuje pridať kľúč do Info.plist (napríklad NSCameraUsageDescription) alebo permission do AndroidManifest.xml, musíte to spraviť cez config plugin. Príklad plugin-u, ktorý pridáva do defaultu permission a usage description:

// plugin/withSecureHash.ts
import { ConfigPlugin, withInfoPlist, withAndroidManifest, AndroidConfig } from 'expo/config-plugins';

interface Options {
  cameraUsageDescription?: string;
}

const withSecureHash: ConfigPlugin<Options> = (config, opts = {}) => {
  config = withInfoPlist(config, (cfg) => {
    cfg.modResults.NSCameraUsageDescription =
      opts.cameraUsageDescription ?? 'Používame kameru na overenie totožnosti.';
    return cfg;
  });

  config = withAndroidManifest(config, (cfg) => {
    AndroidConfig.Permissions.addPermission(cfg.modResults, 'android.permission.CAMERA');
    return cfg;
  });

  return config;
};

export default withSecureHash;

Konzument modulu ho v app.json pridá do plugins poľa a všetka konfigurácia zostáva verzovaná v jednom mieste. Žiadne ručné úpravy Xcode projektu, žiadne merge konflikty v project.pbxproj. Toto je pravdepodobne dôvod č. 1, prečo náš tím prešiel z čistých React Native CLI projektov na Expo. Reprodukovateľné buildy sú v roku 2026 hygiena.

Inline moduly vs. samostatný package v monorepo

Nie každý natívny kód potrebuje samostatný npm balík. Od Expo SDK 53 sú dostupné inline moduly, teda Swift a Kotlin súbory môžete uložiť priamo do modules/ adresára apky a Expo ich autolinkuje ako by boli súčasť package-u. Kedy použiť čo:

  • Inline modul. Jednorazová funkcionalita používaná v jednej apke, prototypovanie, thin wrapper okolo systémového API bez potreby zdieľania.
  • Samostatný package. Zdieľanie medzi apkami v monorepo, verejný open-source modul, netriviálna doménová vrstva s testami.

V mojom fintech monorepo držíme dva interné packagey (@bank/native-crypto, @bank/native-nfc) a všetko ostatné je inline. Pravidlo palca: ak sa modul referuje z viac než jednej apky alebo má vlastnú testovaciu suite, dostane vlastný balík. Inline moduly sú výborné pre experimenty, ale nemôžete ich verzovať nezávisle od apky.

Testovanie, ladenie a upgrade playbook

Natívne moduly sa najlepšie testujú v example apke, ktorú create-expo-module vygeneruje. Pre CI odporúčam:

  1. Unit testy Swift/Kotlin v natívnom testovacom targete, izolované od React Native runtime. XCTest a JUnit stačia. Nepotrebujete simulátor.
  2. Integračné testy cez Maestro alebo Detox v example apke. Overia, že bridge funguje end-to-end.
  3. Type-check verejného TS API cez tsc --noEmit. Vyhne sa driftu medzi JS wrapperom a natívnou implementáciou.

Pri ladení vám pomôže logging cez ExpoModulesCore.log, ktorý sa zobrazí v Metro terminál aj v React Native DevTools 2026 pre debugovanie po konci Flippera. Pri page fault-och v Swifte použite Xcode Debug Navigator; Kotlin výnimky si prezriete cez adb logcat s filterom ExpoModulesCore. Pre pokročilejšie zoznamy a testovaciu apku sa oplatí zladiť aj s porovnaním Maestro a Detox pre testovanie React Native aplikácií. Osobne mi Maestro ušetril hodiny pri prvej integrácii SignaturePad modulu.

Upgrade playbook medzi SDK verziami

Expo Modules API dodržiava semver, ale breaking changes sa dejú medzi major verziami. Môj upgrade playbook pre platform tím:

  1. Zamknite modul na konkrétnu Expo SDK verziu v peerDependencies (napr. "expo": "^53.0.0").
  2. Pri upgrade najskôr updatnite example apku, zbežte integračné testy, potom až konzumujúce apky.
  3. Sledujte expo-module-scripts release notes. Obsahujú migračné poznámky pre DSL zmeny.
  4. Config plugin API sa mení pomalšie než runtime API; testujte prebuild output cez expo prebuild --clean.

Pre autoritatívne referencie použite oficiálny Module API Reference na docs.expo.dev, ktorý pokrýva každý DSL prvok. Pre C++ interop scenáre a rozhodovaciu tabuľku pozrite Design considerations v Expo Modules dokumentácii. A pre praktický tutoriál config plugin-u vrátane native module-u sledujte oficiálny Expo Modules + config plugin tutoriál. Ak potrebujete referenčný Swift kód priamo z reálneho SDK, pozrite si zdrojové súbory expo-modules-core na GitHube.

Často kladené otázky

Aký je rozdiel medzi Expo Modules API a Turbo Modules?

Expo Modules API poskytuje Swift/Kotlin DSL s auto-generovanými bindings, zatiaľ čo Turbo Modules využívajú codegen z Flow/TS spec a poskytujú prvotriedny C++ interop. Ak nepotrebujete C++ interoperabilitu, Expo Modules znižujú boilerplate o 60 až 70 % pri porovnateľnej priepustnosti.

Sú Expo Modules kompatibilné s React Native New Architecture?

Áno, plne. Všetky Expo Modules bežia priamo cez JSI v bridgeless móde a sú kompatibilné s Fabric renderer-om aj Turbo Modules. Zároveň zachovávajú spätnú kompatibilitu so starou architektúrou pre postupný upgrade.

Môžem používať Expo Modules v bare React Native projekte bez Expo?

Áno. Stačí pridať package expo a povoliť expo autolinking v Podfile a settings.gradle. Nepotrebujete Expo Router ani EAS, aby ste ťažili z Expo Modules API. Je to nezávislá vrstva.

Ako pridám natívnu závislosť ako CocoaPod alebo Gradle knižnicu?

V ios/<ModuleName>.podspec pridajte s.dependency 'PodName', '~> 1.0'. Pre Android v android/build.gradle pridajte do dependencies bloku štandardný implementation "group:artifact:version". Autolinking sa postará o zvyšok.

Kedy použiť inline modul namiesto samostatného package-u?

Inline modul je vhodný pre jednorazovú funkcionalitu používanú len v jednej aplikácii alebo pre rýchle prototypy. Samostatný package si zvoľte, keď potrebujete modul zdieľať medzi apkami v monorepo, chcete nezávislé verzovanie alebo plánujete open-source distribúciu.

Yelena Petrov
O Autorovi Yelena Petrov

React Native architect at a fintech. Builds platform teams, type-safe bridges, and runs the upgrade playbook so others don't have to.