Quick start
This walks the shortest path from nothing to a working round trip: define a contract, generate the native bindings, implement it natively (Kotlin or Swift), consume it from React Native.
-
Define a contract in a
*.contract.tsfile. Import only from the pure@axion/bridgekit/contractentrypoint.connect-host.contract.ts import { defineContract, t } from '@axion/bridgekit/contract';export const ConnectHost = defineContract('connect.host', {methods: {isLoggedIn: t.query(t.boolean()),installEsim: t.query(t.object({ url: t.string(), iccId: t.string() }),t.literals('success', 'already-installed', 'cancelled', 'error'),{ timeoutMs: null },),},streams: { otpCodes: t.stream(t.string()) },state: { connectivity: t.state(t.object({ online: t.boolean() }), { online: false }) },}); -
Generate the native bindings with the axion CLI. Output is committed, readable code — no hidden directories. Pick the target platform with
--platform:Terminal window # Android (default platform is kotlin)axion bridgekit generate --contracts 'src/**/*.contract.ts' --out-dir bridgekit/generated# iOS — same contracts, Swift bindingsaxion bridgekit generate --contracts 'src/**/*.contract.ts' --platform swift --out-dir ios/contracts/generated -
Implement the provider on the side that owns the capability. Here native owns it — implement the generated interface (Kotlin) or protocol (Swift):
class ConnectHostProvider : ConnectHost {override suspend fun isLoggedIn(): Boolean = session.isLoggedIn()override suspend fun installEsim(params: InstallEsimParams): InstallEsimResult = /* ... */override fun otpCodes(): Flow<String> = smsRetriever.codes()override val connectivity = MutableStateFlow(Connectivity(online = true))}BridgeKit.default.provide(ConnectHostContract, Scope.Global) { ConnectHostProvider() }import AxionContractsfinal class ConnectHostProvider: ConnectHost {func isLoggedIn() throws -> Bool { session.isLoggedIn() }func installEsim(_ params: InstallEsimParams) async throws -> String { /* ... */ }func otpCodes() -> AsyncStream<String> { smsRetriever.codes() }var connectivity: AsyncStream<Connectivity> { /* yield current + updates */ }}import BridgeKit_ = BridgeKitRuntime.default.provide(ConnectHostContract(), scope: .global) {ConnectHostProvider()} -
Consume it from React Native with a typed proxy and the state/stream hooks.
import { useBridge, useBridgeState } from '@axion/bridgekit';function Connect() {const connect = useBridge(ConnectHost);const { value: net } = useBridgeState(ConnectHost, 'connectivity');const onInstall = async () => {const result = await connect.installEsim({ url, iccId });// result: 'success' | 'already-installed' | 'cancelled' | 'error'};return <Button disabled={!net.online} onPress={onInstall} />;} -
Subscribe to a stream — no
useEffectdance, no event-emitter names.const codes = connect.otpCodes();const unsubscribe = codes.subscribe((code) => setOtp(code));// …or: for await (const code of codes) { ... }
The other direction in one line
Section titled “The other direction in one line”When RN provides and native consumes, the shape is identical — only the implementer moves:
useProvideBridge(LiaFeature, { getUnreadCount: () => store.unread });val lia = bridgekit.consume(LiaFeatureContract)val count = lia.getUnreadCount()lia.sessionStatus.collect { value -> /* BridgeValue<SessionStatus> */ }Next: wire BridgeKit into a real project in Installation, understand the building blocks in Defining a contract, or see a complete real app in the demo walkthrough.