返回 Skills
expo/skills· MIT 内容可用

expo-module

Framework (OSS). Guide for creating and writing Expo native modules and views using the Expo Modules API (Swift, Kotlin, TypeScript). Covers module definition DSL, native views, shared objects, config plugins, lifecycle hooks, autolinking, and type system. Use when building or modifying native modules for Expo.

安装

与 skills.sh 相同的 Command / Prompt 安装方式


name: expo-module description: Framework (OSS). Guide for creating and writing Expo native modules and views using the Expo Modules API (Swift, Kotlin, TypeScript). Covers module definition DSL, native views, shared objects, config plugins, lifecycle hooks, autolinking, and type system. Use when building or modifying native modules for Expo. version: 1.0.0 license: MIT

Writing Expo Modules

Complete reference for building native modules and views using the Expo Modules API. Covers Swift (iOS), Kotlin (Android), and TypeScript.

When to Use

  • Creating a new Expo native module or native view
  • Adding native functionality (camera, sensors, system APIs) to an Expo app
  • Wrapping platform SDKs for React Native consumption
  • Building config plugins that modify native project files
  • Adding Android, Apple, or web support to an existing Expo module
  • Editing expo-module.config.json, config plugins, or lifecycle hooks

References

Consult these resources as needed:

references/
  create-expo-module.md      Scaffolding and add-platform-support workflow, defaults, and quirks
  native-module.md           Module definition DSL: Name, Function, AsyncFunction, Property, Constant, Events, type system, shared objects
  native-view.md             Native view components: View, Prop, EventDispatcher, view lifecycle, ref-based functions
  lifecycle.md               Lifecycle hooks: module, iOS app/AppDelegate, Android activity/application listeners
  config-plugin.md           Config plugins: modifying Info.plist, AndroidManifest.xml, reading values in native code
  module-config.md           expo-module.config.json fields, file placement, and autolinking behavior

Quick Start

Prefer create-expo-module over manually creating native module files and directories. In practice, the best path is usually to create the scaffold first and then build on top of it. The scaffold sets up the expected layout, expo-module.config.json, podspec or Gradle files, TypeScript bindings, and the standalone example app flow.

If an existing Expo module only needs another platform, use create-expo-module add-platform-support instead of manually copying native directories.

See references/create-expo-module.md before scaffolding or extending a module. It covers:

  • local vs standalone modules
  • --platform, --features, --barrel, --package-manager, and non-interactive mode
  • expo.autolinking.nativeModulesDir
  • add-platform-support behavior and quirks

Recommended Workflow

  1. Choose the scaffold type first:
    • Local module for one app
    • Standalone module for reuse, monorepos, or publishing
  2. Determine native expo-module features that you will need.
    • Based on the user's instructions determine which feature scaffolding will be useful.
    • Available features: Constant, Function, AsyncFunction, Event, View, ViewEvent, SharedObject
  3. Scaffold deliberately:
    • pass an explicit slug or path
    • choose --platform intentionally instead of relying on defaults
    • use --features to choose code samples which you will modify in the next step to match the real implementation.
  4. Replace generated example code with the real implementation.
  5. If you add a new platform later, prefer add-platform-support over manual file copying.

Practical Scaffolding Rules

  • Feature examples are opt-in. A newly scaffolded module may be minimal if no features were selected.
  • ViewEvent implies View.
  • Local modules do not generate an index.ts barrel by default. Use --barrel only if you want one.
  • In non-interactive local scaffolding, pass the positional slug or path explicitly. --name changes the native class name, not the folder name.
  • Local modules live in expo.autolinking.nativeModulesDir when configured, otherwise in modules/.
  • Standalone modules have their own package metadata, scripts, and usually an example app. Local modules use the host app's tooling instead.

Core File Shapes

The Swift and Kotlin DSL share the same structure. Swift is usually the clearest primary example; consult the references for feature-specific details.

Module Structure Reference

The Swift and Kotlin DSL share the same structure. Both platforms are shown here for reference — in other reference files, Swift is shown as the primary language unless the Kotlin pattern meaningfully differs.

Swift (iOS):

import ExpoModulesCore

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

    Function("hello") { (name: String) -> String in
      return "Hello \(name)!"
    }
  }
}

Kotlin (Android):

package expo.modules.mymodule

import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition

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

    Function("hello") { name: String ->
      "Hello $name!"
    }
  }
}

TypeScript:

import { requireNativeModule } from "expo";

const MyModule = requireNativeModule("MyModule");

export function hello(name: string): string {
  return MyModule.hello(name);
}

expo-module.config.json

{
  "platforms": ["android", "apple"],
  "apple": {
    "modules": ["MyModule"]
  },
  "android": {
    "modules": ["expo.modules.mymodule.MyModule"]
  }
}

Note: iOS uses just the class name; Android uses the fully-qualified class name (package + class). See references/module-config.md for all fields.

附带文件

agents/openai.yaml
interface:
  display_name: "Expo Module"
  short_description: "Build Expo native modules and native views with Swift, Kotlin, TypeScript, autolinking, and config plugins"
  default_prompt: "Use $expo-module to scaffold, implement, clean up, or review Expo native modules, native views, shared objects, lifecycle hooks, autolinking configuration, and config plugins."
references/config-plugin.md
# Config Plugins Reference

Config plugins customize native Android and iOS projects generated with `npx expo prebuild`. They are synchronous functions that accept an `ExpoConfig` and return a modified version.

## Plugin Structure

```
my-module/
  plugin/
    tsconfig.json
    src/
      index.ts
  app.plugin.js         # Entry: module.exports = require('./plugin/build');
```

## Writing a Plugin

Plugin functions follow the `with` prefix naming convention.

```typescript
import {
  ConfigPlugin,
  withInfoPlist,
  withAndroidManifest,
  AndroidConfig,
} from "expo/config-plugins";

const withMyConfig: ConfigPlugin<{ apiKey: string }> = (config, { apiKey }) => {
  // iOS: modify Info.plist
  config = withInfoPlist(config, (config) => {
    config.modResults["MY_API_KEY"] = apiKey;
    return config;
  });

  // Android: modify AndroidManifest.xml
  config = withAndroidManifest(config, (config) => {
    const mainApp =
      AndroidConfig.Manifest.getMainApplicationOrThrow(config.modResults);
    AndroidConfig.Manifest.addMetaDataItemToMainApplication(
      mainApp,
      "MY_API_KEY",
      apiKey
    );
    return config;
  });

  return config;
};

export default withMyConfig;
```

## Using in app.json

```json
{
  "expo": {
    "plugins": [["my-module", { "apiKey": "secret_key" }]]
  }
}
```

## Reading Config Values in Native Code

**Swift:**

```swift
Function("getApiKey") {
  return Bundle.main.object(forInfoDictionaryKey: "MY_API_KEY") as? String
}
```

**Kotlin:**

```kotlin
Function("getApiKey") {
  val appInfo = appContext?.reactContext?.packageManager?.getApplicationInfo(
    appContext?.reactContext?.packageName.toString(),
    PackageManager.GET_META_DATA
  )
  return@Function appInfo?.metaData?.getString("MY_API_KEY")
}
```

## Key Rules

- Plugins must be synchronous; return values must be serializable (except `mods`)
- `Mods` are async functions invoked during the prebuild "syncing" phase
- Use `npm run build plugin` to compile TypeScript plugins
- Test with `npx expo prebuild --clean`
references/create-expo-module.md
# create-expo-module

Use `create-expo-module` to scaffold new Expo modules and `create-expo-module add-platform-support` to extend existing Expo modules.

Prefer `create-expo-module` over manually creating module files and directories. In most cases, the right move is to generate the scaffold first and then build on top of it.

## Choose the Module Type First

### Local module

Use a local module when the native code only belongs to one Expo app.

- lives inside the app
- uses the app's dependencies and tooling
- does not create an example app
- respects `package.json:expo.autolinking.nativeModulesDir`, or falls back to `modules/`

### Standalone module

Use a standalone module when the module should be reusable across apps, live in a monorepo package, or be published to npm.

- has its own `package.json`
- installs its own dependencies
- builds TypeScript during scaffolding
- usually creates an `example` app unless `--no-example` is passed
- may initialize a Git repo if not already inside one

When creating a standalone module, default to keeping the example app. Only skip it when the user explicitly asks for `--no-example` or clearly does not want the example project.

## Recommended Commands

### Local module

Use an explicit slug or path.

```bash
npx create-expo-module@latest key-value-store --local --platform apple android --features Function AsyncFunction
```

If you need deterministic non-interactive output, pass the slug or path explicitly and then pass the rest of the options:

```bash
EXPO_NONINTERACTIVE=1 npx create-expo-module@latest key-value-store \
  --local \
  --name KeyValueStore \
  --package expo.modules.keyvaluestore \
  --platform apple android \
  --features Function AsyncFunction
```

Important quirk:

- in non-interactive local scaffolding, omitting the positional slug causes the CLI to fall back to `my-module`
- `--name` changes the native module class name, not the directory name

### Standalone module

```bash
npx create-expo-module@latest expo-key-value-store --platform apple android --features Function AsyncFunction
```

## Creation Options

These are the module creation options exposed by the CLI:

| Option                        | Applies to        | Notes                                                                                                     |
| ----------------------------- | ----------------- | --------------------------------------------------------------------------------------------------------- |
| `[path]`                      | local, standalone | Positional slug or target path. Use this explicitly for stable local scaffolding in non-interactive mode. |
| `--local`                     | local             | Create a local module inside the current Expo project.                                                    |
| `--platform <platforms...>`   | local, standalone | Valid values: `apple`, `android`, `web`.                                                                  |
| `--features <features...>`    | local, standalone | Pick generated feature examples. Use `all` to include everything.                                         |
| `--full-example`              | local, standalone | Equivalent to `--features all`.                                                                           |
| `--barrel`                    | local             | Generate a local `index.ts` barrel. Ignored for standalone modules.                                       |
| `--source <source_dir>`       | local, standalone | Use a local `expo-module-template` directory instead of downloading from npm.                             |
| `--name <name>`               | local, standalone | Native module name, for example `KeyValueStore`.                                                          |
| `--description <description>` | standalone        | Package description.                                                                                      |
| `--package <package>`         | local, standalone | Android package name, for example `expo.modules.keyvaluestore`.                                           |
| `--author-name <name>`        | standalone        | Package author name.                                                                                      |
| `--author-email <email>`      | standalone        | Package author email.                                                                                     |
| `--author-url <url>`          | standalone        | Package author profile URL.                                                                               |
| `--repo <url>`                | standalone        | Package repository URL.                                                                                   |
| `--license <license>`         | standalone        | Package license identifier.                                                                               |
| `--module-version <version>`  | standalone        | Initial package version.                                                                                  |
| `--package-manager <manager>` | standalone        | One of `npm`, `pnpm`, `yarn`, `bun`.                                                                      |
| `--with-readme`               | standalone        | Keep `README.md` in the generated package.                                                                |
| `--with-changelog`            | standalone        | Keep `CHANGELOG.md` in the generated package.                                                             |
| `--no-example`                | standalone        | Skip creating the example app.                                                                            |

Notes:

- `--description`, author flags, `--repo`, `--license`, and `--module-version` only affect standalone modules because local modules do not have a standalone package manifest.
- `--with-readme`, `--with-changelog`, `--no-example`, and `--package-manager` are standalone-only concerns.
- `--barrel` only affects local modules.
- `--name` changes the native module class name. It does not rename the local module directory.

## Platforms

Valid values are:

- `apple`
- `android`
- `web`

Behavior to remember:

- standalone modules default to all platforms when `--platform` is omitted in non-interactive mode
- local modules also default to all platforms in non-interactive mode
- interactive local scaffolding preselects platforms from `app.json:expo.platforms` when available, mapping `ios` to `apple`
- invalid platform values are ignored with a warning; if all provided values are invalid, the CLI falls back to all platforms

If you do not want web support, omit `web` during scaffolding instead of removing it later.

## Feature Examples

Feature examples are generated starter snippets, not capability restrictions. They are small working examples of common Expo Modules API patterns.

Available values:

- `Constant`
- `Function`
- `AsyncFunction`
- `Event`
- `View`
- `ViewEvent`
- `SharedObject`

Important behaviors:

- no features selected means a minimal module
- in interactive mode, feature examples start unselected
- in non-interactive mode, no features are included unless `--features` or `--full-example` is passed
- `--full-example` is equivalent to `--features all`
- `ViewEvent` automatically includes `View`

Use `View` only when the module actually renders UI. For native-only modules, do not scaffold the view files unless you plan to use them.

## Local Module Quirks

- local modules do not generate an `index.ts` barrel by default
- use `--barrel` only if you want a root barrel file
- local modules skip dependency installation and do not create an `example` app
- local modules do not have a local `package.json`; they rely on the host app

If `--barrel` is not used, the CLI's follow-up instructions point to direct imports from the module's `src/` files.

## Standalone Module Quirks

- `--package-manager` is only relevant for standalone modules
- if omitted, the CLI detects the package manager from the user agent or available package managers
- the scaffold builds TypeScript after installing dependencies
- the `example` app is created only when examples are enabled; `--no-example` skips it
- `--with-readme` and `--with-changelog` opt into those files

The generated standalone scripts include `build`, `clean`, `test`, `prepare`, `open:ios`, and `open:android`.

## add-platform-support

Use this subcommand when an existing Expo module needs another supported platform.

Interactive usage from the module root:

```bash
npx create-expo-module@latest add-platform-support
```

Explicit usage:

```bash
npx create-expo-module@latest add-platform-support --platform android
```

You can also pass the module path:

```bash
npx create-expo-module@latest add-platform-support ./packages/expo-key-value-store --platform web
```

Important behaviors:

- supported values are `apple`, `android`, and `web`
- in non-interactive mode, `--platform` is required
- the command only adds platforms that are not already present in `expo-module.config.json`
- it refuses to overwrite existing `android/` or `ios/` directories
- for native modules, it only works with modules that use the Expo Modules API DSL
- older module formats are not supported

### Feature detection

`add-platform-support` tries to detect the existing module's feature examples from the native module definition. This is best effort.

Use `--features` to override the detected feature examples when:

- the module is unusual
- the module uses generated code
- the definition is spread across multiple files
- the generated files do not match the existing module's shape

If no features are detected or provided, the command creates a minimal scaffold for the new platform.

## Environment Variables

- `EXPO_BETA`: use the next template version
- `EXPO_DEBUG`: enable debug logs
- `EXPO_NO_TELEMETRY`: disable telemetry
- `EXPO_NONINTERACTIVE`: force non-interactive mode
- `CI`: same as `EXPO_NONINTERACTIVE`, used in CI environments
references/lifecycle.md
# Lifecycle Hooks Reference

## Module Lifecycle (in module definition)

```swift
OnCreate {
  // Module initialized - preferred over class initializers
}

OnDestroy {
  // Module deallocated - clean up resources
}

OnAppContextDestroys {
  // App context is being deallocated
}
```

## iOS App Lifecycle (in module definition)

```swift
OnAppEntersForeground { /* UIApplication.willEnterForegroundNotification */ }
OnAppEntersBackground { /* UIApplication.didEnterBackgroundNotification */ }
OnAppBecomesActive { /* UIApplication.didBecomeActiveNotification */ }
```

## Android Activity Lifecycle (in module definition)

```kotlin
OnActivityEntersForeground { /* Activity resumed */ }
OnActivityEntersBackground { /* Activity paused */ }
OnActivityDestroys { /* Activity destroyed */ }
OnNewIntent { intent -> /* Deep link received */ }
OnActivityResult { activity, result -> /* startActivityForResult callback */ }
OnUserLeavesActivity { /* User-initiated background transition */ }
RegisterActivityContracts { /* Modern activity result contracts */ }
```

---

## iOS AppDelegate Subscribers

For hooking into AppDelegate events without editing AppDelegate directly. Requires app's AppDelegate to extend `ExpoAppDelegate`.

```swift
import ExpoModulesCore

public class MyAppDelegateSubscriber: ExpoAppDelegateSubscriber {
  public func applicationDidBecomeActive(_ application: UIApplication) {}
  public func applicationWillResignActive(_ application: UIApplication) {}
  public func applicationDidEnterBackground(_ application: UIApplication) {}
  public func applicationWillEnterForeground(_ application: UIApplication) {}
  public func applicationWillTerminate(_ application: UIApplication) {}

  public func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    return false  // Return true if handled
  }
}
```

Register in `expo-module.config.json`:

```json
{
  "apple": {
    "appDelegateSubscribers": ["MyAppDelegateSubscriber"]
  }
}
```

Result aggregation:
- `didFinishLaunchingWithOptions`: Returns `true` if **any** subscriber returns `true`
- `didReceiveRemoteNotification`: Priority: `failed` > `newData` > `noData`

---

## Android Lifecycle Listeners

For hooking into Activity/Application lifecycle outside module definitions. Useful for handling deep links, intents, and app-level initialization.

### ReactActivityLifecycleListener

Supported callbacks: `onCreate`, `onResume`, `onPause`, `onDestroy`, `onNewIntent`, `onBackPressed`.

> Note: `onStart` and `onStop` are **not supported** — the implementation hooks into `ReactActivityDelegate` which lacks these methods.

```kotlin
class MyPackage : Package {
  override fun createReactActivityLifecycleListeners(
    activityContext: Context
  ): List<ReactActivityLifecycleListener> {
    return listOf(MyActivityListener())
  }
}

class MyActivityListener : ReactActivityLifecycleListener {
  override fun onCreate(activity: Activity, savedInstanceState: Bundle?) { }
  override fun onResume(activity: Activity) { }
  override fun onPause(activity: Activity) { }
  override fun onDestroy(activity: Activity) { }
  override fun onNewIntent(intent: Intent?): Boolean { return false }
  override fun onBackPressed(): Boolean { return false }
}
```

### ApplicationLifecycleListener

Supported callbacks: `onCreate`, `onConfigurationChanged`.

```kotlin
class MyPackage : Package {
  override fun createApplicationLifecycleListeners(
    context: Context
  ): List<ApplicationLifecycleListener> {
    return listOf(MyAppListener())
  }
}

class MyAppListener : ApplicationLifecycleListener {
  override fun onCreate(application: Application) {
    // App-level initialization
  }
}
```
references/module-config.md
# Module Configuration Reference

## expo-module.config.json

Use `expo-module.config.json` for autolinking and module registration.

File placement depends on the module type:

- **standalone module**: place it at the package root, next to `package.json`
- **local module**: place it at the module root inside the app's local modules directory (`expo.autolinking.nativeModulesDir`, or `modules/` by default)

Example:

```json
{
  "platforms": ["android", "apple", "web"],
  "apple": {
    "modules": ["MyModule"],
    "appDelegateSubscribers": ["MyAppDelegateSubscriber"]
  },
  "android": {
    "modules": ["expo.modules.mymodule.MyModule"]
  }
}
```

### Fields

| Field | Description |
|-------|-------------|
| `platforms` | Array of supported platforms. Valid values include `android`, `apple`, `web`, and `devtools`. You can also use granular Apple platforms such as `ios`, `macos`, and `tvos`, but `apple` is preferred when one Swift module supports multiple Apple targets. |
| `apple.modules` | Swift module class names |
| `apple.appDelegateSubscribers` | Swift AppDelegate subscriber class names |
| `android.modules` | Fully-qualified Kotlin module class names (package + class) |

## Autolinking

Expo autolinking automatically discovers and links modules that have `expo-module.config.json`. No manual native project configuration needed — install via npm, run `pod install`.

- standalone modules are resolved from dependencies and search paths.
- local modules are resolved from the `modules` directory or `nativeModulesDir` if defined.

### Resolution Order

1. Explicit dependencies in `react-native.config.js`
2. Custom `searchPaths` directories
3. Local `nativeModulesDir` (defaults to `./modules/`)
4. Recursive npm dependency resolution
references/native-module.md
# Native Module DSL Reference

Swift is shown as the primary language. Kotlin follows the same DSL structure (see SKILL.md for both). Kotlin-specific syntax is noted where it meaningfully differs.

## Name

Sets the module identifier used in JavaScript.

```swift
Name("MyModule")
```

## Constant

Computed once on first access, then cached.

```swift
Constant("PI") { 3.14159 }
```

## Function (Synchronous)

Blocks the JS thread until completion. Supports up to 8 arguments.

```swift
Function("add") { (a: Int, b: Int) -> Int in
  return a + b
}
```

## AsyncFunction

Returns a Promise. Runs on a background thread by default.

```swift
AsyncFunction("fetchData") { (url: URL) -> String in
  let data = try Data(contentsOf: url)
  return String(data: data, encoding: .utf8) ?? ""
}

// Force main queue execution
AsyncFunction("updateUI") { () -> Void in
  // UI work
}.runOnQueue(.main)
```

**Kotlin differences:**

```kotlin
// Supports Kotlin coroutines
AsyncFunction("fetchData") Coroutine { url: java.net.URL ->
  withContext(Dispatchers.IO) {
    url.readText()
  }
}
```

## Property

Getter/setter for JS object properties.

```swift
// Read-only
Property("version") { "1.0.0" }

// Read-write
Property("volume")
  .get { () -> Float in self.volume }
  .set { (newValue: Float) in self.volume = newValue }
```

## Events

Declares events the module can send to JS. Must be declared before using `sendEvent`.

```swift
// Declaration
Events("onChange", "onError")

// Sending from native (Swift)
sendEvent("onChange", ["value": newValue])
```

**Kotlin difference** — uses `bundleOf`:

```kotlin
sendEvent("onChange", bundleOf("value" to newValue))
```

**JS subscription:**

```typescript
import { useEvent } from "expo";
import MyModule from "./MyModule";

// Hook-based (recommended)
const event = useEvent(MyModule, "onChange");

// Manual subscription
const subscription = MyModule.addListener("onChange", (event) => {
  console.log(event.value);
});
// Clean up: subscription.remove()
```

### OnStartObserving / OnStopObserving

Called when the first listener attaches / last listener detaches. Can be scoped to specific events.

```swift
OnStartObserving("onChange") {
  // Start producing events
}

OnStopObserving("onChange") {
  // Stop producing events
}
```

---

## Type System

### Primitives

| Swift | Kotlin | JS |
|-------|--------|----|
| `Bool` | `Boolean` | `boolean` |
| `Int`, `Int32` | `Int` | `number` |
| `Int64` | `Long` | `number` |
| `Float`, `Float32` | `Float` | `number` |
| `Double` | `Double` | `number` |
| `String` | `String` | `string` |
| `URL` | `java.net.URL` / `android.net.Uri` | `string` |
| `CGPoint` | - | `{ x, y }` |
| `CGSize` | - | `{ width, height }` |
| `CGRect` | - | `{ x, y, width, height }` |
| `UIColor` / `CGColor` | `android.graphics.Color` | `string` (ProcessedColorValue) |
| `Data` | `kotlin.ByteArray` | `Uint8Array` |

### Records (Struct-like types)

```swift
struct UserRecord: Record {
  @Field var name: String = ""
  @Field var age: Int = 0
  @Field var email: String?
}

Function("createUser") { (user: UserRecord) -> Bool in
  return true
}
```

**Kotlin difference** — uses `class` instead of `struct`, optional fields need explicit `= null`:

```kotlin
class UserRecord : Record {
  @Field var name: String = ""
  @Field var age: Int = 0
  @Field var email: String? = null
}
```

### Enums (Enumerable)

```swift
enum Theme: String, Enumerable {
  case light
  case dark
  case system
}

Function("setTheme") { (theme: Theme) in
  // type-safe enum value
}
```

**Kotlin difference** — uses `enum class` with explicit `value` property:

```kotlin
enum class Theme(val value: String) : Enumerable {
  LIGHT("light"),
  DARK("dark"),
  SYSTEM("system")
}
```

### Either Types (Union types)

```swift
Function("process") { (input: Either<String, Int>) in
  if let str = input.get(String.self) {
    // handle string
  } else if let num = input.get(Int.self) {
    // handle number
  }
}
```

Also available: `EitherOfThree<A, B, C>`, `EitherOfFour<A, B, C, D>`.

### JavaScript Values (Direct JS manipulation)

For advanced use in synchronous functions running on JS thread:

```swift
Function("callback") { (fn: JavaScriptFunction<String>) in
  let result = fn("arg1", "arg2")
}
```

---

## Shared Objects

Bridge native class instances to JS with automatic lifecycle management. Instances are deallocated when neither JS nor native code holds a reference.

### Defining a Shared Object

```swift
class ImageContext: SharedObject {
  private var image: UIImage

  init(image: UIImage) {
    self.image = image
    super.init()
  }

  func rotate(degrees: Double) {
    image = image.rotated(degrees: degrees)
  }
}
```

**Kotlin difference** — takes `RuntimeContext` in constructor, override `sharedObjectDidRelease()` for cleanup:

```kotlin
class ImageContext(
  runtimeContext: RuntimeContext,
  private var bitmap: Bitmap
) : SharedObject(runtimeContext) {

  fun rotate(degrees: Float) { /* ... */ }

  override fun sharedObjectDidRelease() {
    if (!bitmap.isRecycled) bitmap.recycle()
  }
}
```

### Exposing via Class DSL

```swift
Class("Context", ImageContext.self) {
  Constructor { (path: String) -> ImageContext in
    return ImageContext(image: UIImage(contentsOfFile: path)!)
  }

  Function("rotate") { (ctx: ImageContext, degrees: Double) -> ImageContext in
    ctx.rotate(degrees: degrees)
    return ctx
  }

  Property("width")
    .get { (ctx: ImageContext) -> Int in ctx.width }
}
```

Other Class DSL components: `StaticFunction`, `StaticAsyncFunction`, `AsyncFunction`.

### SharedRef

Specialized shared reference for passing typed objects between modules:

```swift
final class ImageRef: SharedRef<UIImage> {}
```

### JS Usage

```typescript
const ctx = await ImageModule.create("/path/to/image.png");
ctx.rotate(90);
console.log(ctx.width);
```
references/native-view.md
# Native View Reference

Native views let you render platform UI components (UIView on iOS, Android View on Android) as React components.

## Defining a View

**Swift:**

```swift
public class MyViewModule: Module {
  public func definition() -> ModuleDefinition {
    Name("MyView")

    View(MyNativeView.self) {
      Prop("title") { (view: MyNativeView, title: String) in
        view.titleLabel.text = title
      }

      Events("onPress", "onLoad")

      AsyncFunction("reset") { (view: MyNativeView) in
        view.reset()
      }
    }
  }
}

class MyNativeView: ExpoView {
  let titleLabel = UILabel()

  required init(appContext: AppContext) {
    super.init(appContext: appContext)
    clipsToBounds = true
    addSubview(titleLabel)
  }

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

**Kotlin:**

```kotlin
class MyViewModule : Module() {
  override fun definition() = ModuleDefinition {
    Name("MyView")

    View(MyNativeView::class) {
      Prop("title") { view: MyNativeView, title: String ->
        view.titleView.text = title
      }

      Events("onPress", "onLoad")

      AsyncFunction("reset") { view: MyNativeView ->
        view.reset()
      }
    }
  }
}

class MyNativeView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
  val titleView = TextView(context).also {
    addView(it, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
  }
}
```

**TypeScript:**

```typescript
import { requireNativeView } from "expo";

export type MyViewProps = {
  title?: string;
  onPress?: (event: { nativeEvent: { x: number; y: number } }) => void;
  onLoad?: () => void;
} & ViewProps;

const NativeView = requireNativeView<MyViewProps>("MyView");

export function MyView(props: MyViewProps) {
  return <NativeView {...props} />;
}
```

## View Event Dispatching

**Swift:**

```swift
class MyNativeView: ExpoView {
  let onPress = EventDispatcher()

  func handleTap(at point: CGPoint) {
    onPress(["x": point.x, "y": point.y])
  }
}
```

**Kotlin:**

```kotlin
class MyNativeView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
  private val onPress by EventDispatcher()

  fun handleTap(x: Float, y: Float) {
    onPress(mapOf("x" to x, "y" to y))
  }
}
```

## View Lifecycle

```swift
// Called after all props have been set
OnViewDidUpdateProps { (view: MyNativeView) in
  view.applyChanges()
}
```

```kotlin
// Android only - called when view is no longer used
OnViewDestroys { view: MyNativeView ->
  view.cleanup()
}
```

## AsyncFunction on Views

Functions defined inside `View` are accessible via React ref:

```typescript
const ref = useRef<MyView>(null);
// Call native function
await ref.current?.reset();
```

## PropGroup (Android)

Batch-register multiple props with shared setter logic:

```kotlin
View(MyNativeView::class) {
  PropGroup("border", "width" to Float::class, "color" to Int::class) { view, index, value ->
    when (index) {
      0 -> view.borderWidth = value as Float
      1 -> view.borderColor = value as Int
    }
  }
}
```

## GroupView (Android)

Enable view group functionality for managing child views:

```kotlin
View(MyContainerView::class) {
  GroupView {
    AddChildView { parent, child, index -> parent.addView(child, index) }
    GetChildCount { parent -> parent.childCount }
    GetChildViewAt { parent, index -> parent.getChildAt(index) }
    RemoveChildView { parent, child -> parent.removeView(child) }
    RemoveChildViewAt { parent, index -> parent.removeViewAt(index) }
  }
}
```
    expo-module | Prompt Minder