Skills Development Scaffold Native Platform Extension Code

Scaffold Native Platform Extension Code

v20260903
generate-native-extension
This skill generates the complete native source code for a third-party Platform Abstraction Module (PAM) control, including iOS Obj-C, Android Kotlin, required podspecs, and a manifest contract. It bypasses TypeScript layers, focusing purely on the native build surface and the dispatch contract (`manifest.json`). This is used by developers to scaffold the foundational, OS-specific code necessary for integrating custom, native modules into a React Native environment.
Get Skill
415 downloads
Overview

/generate-native-extension

Reads PRD.md in the working directory and writes the native sources for a third-party PAM control following the layout in shared/repo-layout.md. This is the native-only (compiled .ppmplugin) track — there is NO TypeScript INativeExtension / handleMessageAsync layer; the wrap host dispatches straight to NativeModules.<Pascal>Module.<method> per the manifest's receivers contract (see shared/ppmplugin-format.md §2). The output is substantially complete native code so the engineer starts at customizing OS-specific code, not writing boilerplate.

This skill writes the native module half of the repo (ios/, android/, optional podspec, dev-only package.json) and the committed ./manifest.json — the dispatch-contract source of truth. The manifest is authored here, alongside the native code it describes, because every field in it is derived from the names this scaffold emits (getName(), the @ReactMethod list, the package class); authoring it now means the Companion PCF (/generate-pcf-companion) reads a real contract instead of re-deriving one, so the composite key <name>/<receiver> can't drift between the PCF and the module. The build stage /generate-ppmplugin-manifest (inside /generate-ppmplugin) then validates + reconciles + stages this manifest rather than authoring it from scratch. The Companion PCF is generated separately by /generate-pcf-companion because it requires pac CLI and a different toolchain.


Step 1 — Read the shared docs and the PRD

Before any write:

  1. Read shared/shared-instructions.md.
  2. Apply the per-skill minimal prereq policy (shared-instructions.md §1.5). This track is self-contained (shared-instructions §0a) and uses only the working tree and public package registries. This skill needs no toolchain to write the files — optionally Node + pnpm to seed the dev-only package.json's devDeps from the public npm registry (used later by /build-android-binary / /build-ios-binary, not here). Step 4's smoke check is a structural self-check — it does NOT compile anything. Run the /generate-native-extension check from prereq-check.md (git required; Node/pnpm optional — there is no "baseline" check in this self-contained track).

Print the prereq status as a visible block per shared-instructions.md §9.2 before continuing:

```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Prereq check — /generate-native-extension
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

🟢 ✓ git installed
🟢 ✓ Node 20+ installed (optional — only to seed package.json devDeps from public npm)
🟢 ✓ pnpm installed (optional — same)

🟢 checks passed. Ready to proceed.
```

If git is missing, print its → Fix: line and STOP. Node/pnpm are optional here — if absent, note them as n/a (devDeps seed deferred to build skills) rather than failing.

  1. Read shared/naming-conventions.md — the derived-identifier table is canonical, including the Module-suffix rule for the native module symbol. Derive all file paths and class names from §2 of the PRD using that table; do not invent.
  2. Read shared/ppmplugin-format.md — §2 (the runtime dispatch contract: <name>/<receiver> → NativeModules.<nativeModule>.<method>, where <nativeModule> = <Pascal>Module) and §4 (the upload-compatibility checks that the native module symbol must satisfy). The native modules this skill emits dispatch straight off that contract — there is NO TS INativeExtension layer mediating; see §3.3 below.
  3. Read shared/repo-layout.md — the exact tree, file list, and package.json shape to emit.
  4. Read ./PRD.md from the current working directory. If missing or empty, STOP with BLOCKED: PRD.md not found — run /design-native-extension-feature first.
  5. Read ./.extension-state.md if present. If the phase shows scaffold-complete, ask the user whether to regenerate (with confirm — overwrites files), resume (only fill in missing files), or abort.

The structural patterns this skill needs to emit (iOS module shape, Android module shape, podspec, package.json) are fully prescribed in this SKILL.md (§3.1–§3.7) and in shared/repo-layout.md. Do NOT fetch the reference extension repo at runtime — its lessons are already encoded here, and fetching it would risk copying PDF-specific code into a non-PDF extension.

If any read fails, STOP and report which file is missing.


Step 2 — Confirm the scaffold plan with the user

Print a concise summary derived from the PRD, then gate on approval before any write.

Scaffold plan
─────────────
Repo: powerapps-<kebab>
package: <kebab>-control  (dev-only, private — not published)
Class: <Pascal>
Native module: <Pascal>Module  → NativeModules.<Pascal>Module  (== ./manifest.json receivers[].nativeModule)
iOS class: RCT<Pascal>Module  (+moduleName returns <Pascal>Module)
Android module: <Pascal>Module  (com.powerapps.<lower>)
Podspec: <Pascal>Extension.podspec (optional, system-frameworks-only)
Dispatch contract: ./manifest.json (committed — written by this skill; read by the PCF + build stage)

Frameworks
  iOS:     <list from ARCHITECTURE §1.2>
  Android: <list from ARCHITECTURE §1.3>

Operations (<count from PRD §4>): <comma-separated names>
Pattern: <one-shot | streaming | two-way>
Error codes: <count from ARCHITECTURE §5>

Target directory: <cwd> (writes <N> files; no existing files will be overwritten without confirm)
Distribution: the compiled `.ppmplugin` bundle (built later by /generate-ppmplugin). This skill is purely local — no remote, no feed, no registry.

Use AskUserQuestion (single-select):

Proceed with this scaffold?
- Yes — generate the files (recommended): write the control's sources into the current directory. This skill does not run git — no git init, no staging, no commit (the control lives in your existing repo; you commit when you're ready).
- Edit the PRD first — exit; user re-runs /design-native-extension-feature to adjust.
- Cancel

Step 3 — Generate the files

Write files in the order below. After each top-level group, print a one-line progress update (✓ wrote ios/ (3 files)). Don't dump file contents — the user sees the diff via the IDE.

Every file path is relative to the current working directory (the repo root). Names are derived per shared/naming-conventions.md.

3.1 Top-level repo files

Write:

  • .gitignore — emit exactly the following entries:
  • Node: node_modules/, dist/, build/
  • .ppmplugin build staging — MANDATORY: ppmplugin/ (the gitignored staging dir where /generate-ppmplugin writes the staged copy of the manifest, the binaries, and the final bundle — never committed. NOTE: the committed source-of-truth manifest.json lives at the repo root (./manifest.json, written below), NOT under ppmplugin/ — do not gitignore it; see shared/ppmplugin-format.md §1)
  • OS / editor: .DS_Store, .idea/, .vscode/
  • Claude Code local state (per-user, not shared): .claude/
  • Env: .env* (but allow !.env.example)
  • iOS build: Pods/, *.xcworkspace, DerivedData/, *.xcodeproj/xcuserdata/
  • Android build: *.iml, .gradle/, local.properties, captures/, .externalNativeBuild/, .cxx/
  • PCF build dirs only — NOT the pcf/ folder itself; source files (index.ts, ControlManifest.Input.xml, package.json, pcfconfig.json, etc.) stay tracked: pcf/**/{out,Solutions,node_modules,obj,bin,generated}/
  • Test-harness artifacts: test-harness/*.msapp
  • Skill-generated backups: *.bak.* (skills that replace tracked content may save a timestamped backup; those are intentionally local-only)
  • Design-time previews: .pcf-preview/ (HTML mockup of the PCF as it appears in Canvas Studio — written by /design-native-extension-feature Step 8.0 for visual review; regenerated each design iteration; not a source-of-truth artifact)
  • package.json — per the dev-only shape in shared/repo-layout.md §"package.json shape (dev-only)". Fill in name (a plain local name, e.g. <kebab>-control) and description from the PRD. version starts at 0.1.0. Set "private": true.

This manifest is never published — no publishConfig, no feed registry, no files array, no main/types, no .npmrc. Its only job is to pin the React Native version the native builds compile against:
```json
{
"name": "-control",
"version": "0.1.0",
"private": true,
"description": "",
"devDependencies": {
"react": "18.2.0",
"react-native": "0.79.7"
}
}
```
The react-native devDep supplies the iOS headers (/build-ios-binary) and pins the react-android coordinate the Android build resolves (/build-android-binary); add any other build-time devDeps the native modules need. All deps resolve from the public npm registry — there is no internal feed.

  • manifest.json (repo root, committed — the dispatch-contract source of truth) — author it now from the names this scaffold emits, per shared/ppmplugin-format.md §2 (schema) + §3 (derivation). This is the single artifact the Companion PCF (/generate-pcf-companion) and the build stage (/generate-ppmplugin-manifest) both read; authoring it here, next to the native code it describes, is what keeps the composite key <name>/<receiver> from drifting between the PCF and the module. Fields:
  • name = kebab(<Pascal>) of the class name (not the repo/capability name) — e.g. class PenInput → pen-input.
  • version = the package.json version (0.1.0).
  • abi = { "compatibleShells": ">=1.0.0", "builtAgainst": "1.0.0" } (default; the build skills don't change it).
  • receivers[] = a single entry { "name": "<Pascal>Extension", "nativeModule": "<Pascal>Module", "methods": [<every @ReactMethod / RCT_EXPORT_METHOD name emitted in §3.4 / §3.5>] }. nativeModule MUST equal Android getName() and the iOS +moduleName return value — the Module-suffixed name (the reserved-name dodge).
  • entrypoints = declare every platform this scaffold generated (so the committed manifest is the full contract; the build stage trims it to the shipped target):
  • Android → "android": { "dex": "<Pascal>Plugin.dex", "packageClass": "com.powerapps.<lower>.<Pascal>Package" }
  • iOS → "ios": { "framework": "<Pascal>Plugin", "moduleClass": "RCT<Pascal>Module" }

This is a logical contract, not a built artifact — it lists the platforms the module supports; the per-platform binaries are compiled later and the staged copy under ppmplugin/staging/ is reconciled down to whatever actually ships. Do NOT emit any entrypoints.js / extension.hbc / extensionClassName / jsLayer field — those are SDK-era leakage /audit-ppmplugin rejects. (The build stage re-runs the full validator on this file, so a malformed manifest is caught either way — but emit it correctly here.)

  • README.md — one-page user-facing doc tailored to the control. Sections: "What's in the box" (the compiled .ppmplugin bundle + PCF companion), "Build" (run /generate-ppmplugin to produce the .ppmplugin), "Architecture" (a Mermaid-or-ASCII diagram of Canvas formula → PCF → wrap-bridge → NativeModules.<Pascal>Module), "Development" (pnpm install to seed devDeps; native code is compiled by the build skills, not here), "Reference docs" (link to shared/ppmplugin-format.md). Use the PRD's §1 Summary verbatim. Drive every section from the PRD — never inject example values, prose, or screenshots from any other control's README.
  • CHANGELOG.md — single entry:

```markdown

Changelog

0.1.0 —

  • Initial scaffold for native control.
  • Generated by pam-native-extensions plugin from PRD.md.

```

  • LICENSE — MIT.

3.2 The podspec (optional, at repo root)

Write <Pascal>Extension.podspec at the repo root (NOT inside ios/) only if ARCHITECTURE §1.2 names additional iOS system frameworks the module links. The .ppmplugin iOS build (/build-ios-binary) compiles from a throwaway staged Xcode project and does NOT npm-autolink against this podspec — so it lists system frameworks only (no React-Core / RN-CLI autolink dependency, no remote source). It exists for local pod lib lint convenience, not the bundle build. Template:

require "json"

package_json = JSON.parse(File.read(File.join(__dir__, "package.json")))

Pod::Spec.new do |s|
  s.name         = "<Pascal>Extension"
  s.version      = package_json["version"]
  s.summary      = "<one-line description from PRD>"
  s.description  = <<-DESC
    <2-3 sentence description from PRD — what it does, what it bridges to>
  DESC
  s.license      = "MIT"
  s.author       = { "Author" => "" }
  s.platform     = :ios, "<min-deployment-target from ARCHITECTURE §1.2>"
  s.source       = { :path => "." }
  s.source_files = "ios/**/*.{h,m}"   # change to {h,m,swift} if Swift used
  s.frameworks   = <comma-quoted list of SYSTEM frameworks from ARCHITECTURE §1.2>
  # No React-Core dependency: the .ppmplugin build resolves RN headers from the
  # react-native devDep in package.json, not via CocoaPods autolinking.
end

3.3 No TypeScript layer — the dispatch contract

This is the native-only track: there is no src/ TypeScript layer, no src/<Pascal>Extension.ts, no src/types.ts, no INativeExtension / handleMessageAsync implementation, and no sendAsync transport. (Those belong to the first-party SDK track — NOT in this track.) Do NOT generate any of them; reintroducing a TS contract layer here produces SDK-era leakage that /audit-ppmplugin rejects.

The contract instead is the manifest's runtime dispatch (shared/ppmplugin-format.md §2): the wrap host routes a call by the composite key <name>/<receiver> straight to NativeModules.<Pascal>Module.<method>(args, promise). There is no JS mediator. This means:

  • The request shape (the args object) and response shape (the object the promise resolves with) from ARCHITECTURE §4 are realized directly in the native @ReactMethod / RCT_EXPORT_METHOD signatures + their JSON responses — see §3.4 (iOS) and §3.5 (Android). The per-operation JSON parsing, request validation, operation branching, and error-code responses that a first-party TS handleMessageAsync would have done are emitted inside each native method instead. That dispatch logic is the valuable part this skill generates.
  • The manifest.json that declares name, receivers[].method, and receivers[].nativeModule (= <Pascal>Module) is written by this skill at the repo root (§3.1) — the native module symbols it emits and the manifest's receivers[] are authored together, so they can't disagree. /generate-ppmplugin-manifest later validates + reconciles + stages this file rather than re-authoring it (§2/§3 below + shared/ppmplugin-format.md §3).
  • The error-code set from ARCHITECTURE §5 is realized as the string codes the native errorJson(code, message) helpers emit (§3.4 / §3.5), each paired with a human-readable message — there is no TS error-union type to declare. These codes are the stable strings from the canonical catalog shared/error-codes.md (Canvas formulas branch on them, so they must not drift); emit exactly the catalog spelling for any code ARCHITECTURE §5 reuses. The PCF reads both: the error code to branch on, the message to surface as its ErrorMessage output.

3.4 iOS (ios/)

Write:

  • ios/RCT<Pascal>Module.h — minimal Obj-C header importing <React/RCTBridgeModule.h>, declaring @interface RCT<Pascal>Module : NSObject <RCTBridgeModule> @end.
  • ios/RCT<Pascal>Module.m — the implementation. Generate complete working code, not TODO placeholders. For each operation in PRD §4, the per-operation §3. block prescribes every implementation decision (framework, hosting, key APIs, export shape, edge case handling). Generate the implementation verbatim from §3.:
  • Imports: include RCT<Pascal>Module.h, UIKit, plus every framework named in ARCHITECTURE §3.'s "Framework / class" field for any operation (e.g. #import <PencilKit/PencilKit.h> if any §3. names PencilKit).
  • Module identity: do NOT emit RCT_EXPORT_MODULE(...) in a wrap plugin framework. That macro registers via +load and _RCTRegisterModule, which is not visible to the framework's dlopen flat namespace. Instead emit a class method + (NSString *)moduleName { return @"<Pascal>Module"; } — the Module-suffixed name. The Obj-C class name stays RCT<Pascal>Module (matching entrypoints.ios.moduleClass), while +moduleName MUST equal the manifest's receivers[].nativeModule and JS sees NativeModules.<Pascal>Module. Do NOT strip the suffix.
  • + (BOOL)requiresMainQueueSetup returning NO unless any §3. requires main-thread init.
  • init safety — the module is instantiated eagerly at load via [cls new], so init MUST NOT throw or do heavy/side-effecting work (ppmplugin-format §5). Do not acquire hardware, register NSNotification/KVO observers, or touch AVCaptureSession/CLLocationManager in init — defer to the first RCT_EXPORT_METHOD call (lazy), and wrap any unavoidable init work in @try/@catch. An uncaught exception in init crashes the host at launch (the iOS analogue of the Android Looper-less-Handler crash).
  • For each operation, write an RCT_EXPORT_METHOD taking exactly one NSDictionary *request parameter, then RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject — e.g. RCT_EXPORT_METHOD(capturePenInput:(NSDictionary *)request resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject). This matches the wrap dispatch contract: the PCF sends args: [request] (a one-element array) spread positionally, so the method's first positional param is the request dictionary (ppmplugin-format §2). Read fields off request (request[@"…"]); do NOT expand into multiple positional params. Also: the Obj-C class MUST instantiate via a no-arg [cls new] after the runtime loads it — don't add a custom designated initializer that takes arguments. The body implements §3.'s iOS spec completely:
  • The hosting setup ("dedicated UIViewController presented modally, full-screen" → emit a UIViewController subclass or inline VC + presentViewController:animated:completion:). The presented VC's viewDidLoad MUST constrain custom content views to view.safeAreaLayoutGuide, not view directly — this prevents content from intruding under the notch / Dynamic Island / home indicator. Set modalPresentationStyle = UIModalPresentationFullScreen (or .pageSheet per ARCHITECTURE §3.). Add a UINavigationBar with Done / Cancel UIBarButtonItems for clear action affordance — same Material-toolbar-equivalent pattern as Android.
  • The key API calls in the order §3. specifies (e.g. PKCanvasView init, PKToolPicker attachment, drawing capture)
  • Each Done/Cancel/dismiss handler as §3. specifies
  • The export step as §3.'s "Export" line specifies (e.g. drawing.image(from: canvas.bounds, scale: 2.0) → PNG → base64)
  • Each edge case from §3.'s "Edge cases handled" list, with the exact behavior named (e.g. "User taps Cancel → resolve with USER_CANCELLED")
  • Threading: background work on dispatch_get_global_queue; UI presentation on dispatch_get_main_queue. Long-running native work must not block the JS thread.
  • Error helper: emit - (NSString *)errorJsonWithCode:(NSString *)code message:(NSString *)message that builds the dict @{@"status": @"error", @"error": code, @"message": (message ?: @"")} and serializes it via NSJSONSerialization — the SAME serializer as the success helper. Do NOT use stringWithFormat: a message (or code) containing a ", \, or newline would emit invalid JSON, which the PCF's response parse would surface as a misleading PARSE instead of the real failure — defeating the whole point of the message. The message is a human-readable diagnostic that makes the failure debuggable from the PCF without a native debugger: for a caught exception pass error.localizedDescription; for a validation failure a specific reason (e.g. @"missing required field 'uri'"); for USER_CANCELLED a short note. Every error path calls this with BOTH a code and a message — never a bare code.
  • Success helper: emit - (NSString *)successJsonWith:(NSDictionary *)result that builds {"status":"ok","result":<result>} via NSJSONSerialization.
  • Error propagation — wrap the operation body so every failure reaches the PCF with a code AND a message. Any framework/runtime failure must resolve with errorJsonWithCode:message: carrying a specific code and reason — never throw an uncaught Obj-C exception, crash, or resolve empty. Use @try/@catch around risky synchronous work and resolve the @catch with INTERNAL_ERROR plus exception.reason.
  • UI hygiene boilerplate for each presented UIViewController's viewDidLoad (mirrors Android's insets handling — prevents the most common iOS issue: content under safe areas, status bar, home indicator):

```objc

  • (void)viewDidLoad {

[super viewDidLoad];
self.view.backgroundColor = [UIColor systemBackgroundColor];

// Navigation bar with Done / Cancel — equivalent to Android's MaterialToolbar.
UINavigationBar *navBar = [[UINavigationBar alloc] init];
navBar.translatesAutoresizingMaskIntoConstraints = NO;
UINavigationItem *navItem = [[UINavigationItem alloc] initWithTitle:@""];
navItem.leftBarButtonItem = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemCancel
target:self action:@selector(handleCancel)];
navItem.rightBarButtonItem = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemDone
target:self action:@selector(handleDone)];
navBar.items = @[navItem];
[self.view addSubview:navBar];

// Content view — the operation-specific surface (e.g. PKCanvasView, AVCaptureVideoPreviewLayer host).
// Constrain to safeAreaLayoutGuide so content doesn't extend under the notch / home indicator.
UIView *contentView = [[UIView alloc] init]; // Replace with operation-specific view per ARCHITECTURE §3.
contentView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:contentView];

[NSLayoutConstraint activateConstraints:@[
[navBar.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor],
[navBar.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor],
[navBar.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor],
[contentView.topAnchor constraintEqualToAnchor:navBar.bottomAnchor],
[contentView.leadingAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.leadingAnchor],
[contentView.trailingAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.trailingAnchor],
[contentView.bottomAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.bottomAnchor],
]];
}

  • (UIStatusBarStyle)preferredStatusBarStyle {

// Adapt to system appearance — matches Android's windowLightStatusBar in light theme.
return UIStatusBarStyleDefault; // automatic light/dark per system
}
```

  • Modal helper: emit - (UIViewController *)topViewController if any operation presents modally:

```objc

  • (UIViewController *)topViewController {

UIViewController *root = UIApplication.sharedApplication.keyWindow.rootViewController;
while (root.presentedViewController) { root = root.presentedViewController; }
return root;
}
```

No TODO placeholders. No // implement this. If a §3. block is incomplete (any "Key APIs and decisions" item is vague or missing), STOP with NEEDS_CONTEXT: ARCHITECTURE §3.<n> implementation block is incomplete — re-run /design-native-extension-feature Step 7 (per-operation implementation walkthrough) to complete it. Don't paper over a vague spec with a guess.

3.5 Android (android/)

Write:

  • android/build.gradle — library-only gradle config. The module is consumed by the host's managed build, which provides the root project setup. (For the standalone .ppmplugin build, /build-android-binary compiles from a throwaway staged copy with pinned versions — this canonical file is never edited; see shared/ppmplugin-format.md §5.) Do NOT emit a buildscript { ... }, allprojects { ... }, or any classpath declarations — those belong to the root project, not this library module.

Library-only shape (this is the entire file — no preamble, no root-project blocks):
```gradle
// -control
// Android library module — consumed by the host's managed build.

apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'

def safeExtGet(prop, fallback) {
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}

android {
namespace "com.powerapps."
compileSdkVersion safeExtGet('compileSdkVersion', 35)
defaultConfig {
minSdkVersion safeExtGet('minSdkVersion', )
targetSdkVersion 35
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions { jvmTarget = '17' }
}

dependencies {
// 'react-android' (renamed from 'react-native' in RN 0.73). compileOnly + pinned:
// the wrap shell provides RN at runtime, so never bundle it, and the legacy
// 'react-native:+' coordinate does not resolve in the standalone build.
// Read from package.json devDependencies (currently 0.79.7).
compileOnly "com.facebook.react:react-android:"
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.core:core-ktx:1.12.0' // WindowCompat / WindowInsetsCompat for UI hygiene
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' // for the generated layout XML
implementation 'com.google.android.material:material:1.11.0' // Material 3 theme + components
// Plus any ARCHITECTURE §1.3 / §1.4-specified additions (e.g. ML Kit, FusedLocationProvider)
}
```

  • Files NOT to generate (these are root-project / standalone-build concerns; the host's managed build — or, for the .ppmplugin, /build-android-binary's staged copy — owns them):
  • android/settings.gradle — root project's responsibility
  • android/gradle.properties — root project's properties; android.useAndroidX and android.enableJetifier are supplied ambiently by the host (and generated into the staged copy by /build-android-binary), not by the library
  • android/gradlew + android/gradle/wrapper/* — the Gradle wrapper; library modules don't need their own wrapper
  • Any top-level buildscript { ext, repositories, dependencies (classpath) } block in build.gradle — the host provides AGP + Kotlin classpaths
No standalone build script. The android/ directory is consumed by the host's managed build (and copied into a pinned staging dir by /build-android-binary for the .ppmplugin); it doesn't have to compile in isolation. Don't add a top-level buildscript { ... } / allprojects { ... } block — the host provides those. Standalone ./gradlew assembleDebug against this directory is not a validation path we support (native compile happens in /build-android-binary, not here — see shared/ppmplugin-format.md §5).
  • android/src/main/AndroidManifest.xml — registers permissions from ARCHITECTURE §1.4 AND the dedicated capture Activity (if ARCHITECTURE §3. hosts in one) with a Material 3 theme:

```xml



android:name=".CaptureActivity"
android:theme="@style/Theme."
android:screenOrientation="portrait"
android:exported="false" />


```

  • android/src/main/res/values/themes.xml — Material 3 theme so all components render with proper Material styling, not the bare AppCompat defaults. Without this, generated UIs hit issues like status bar overlap and unthemed buttons.

```xml




```

  • android/src/main/res/layout/activity_<lower>_capture.xml — root layout uses Material components. Toolbar at top, bounded content area in a MaterialCardView (drawing surface, camera preview, etc.):

```xml

xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res/auto"
android:id="@+id/root"
android:layoutwidth="matchparent"
android:layoutheight="matchparent"
android:background="?attr/colorSurface">

android:id="@+id/toolbar"
android:layoutwidth="matchparent"
android:layout_height="?attr/actionBarSize"
android:elevation="4dp"
app:layoutconstraintToptoTopOf="parent"
app:menu="@menu/capturemenu"
app:navigationIcon="@drawable/ic_close"
app:title="" />

android:id="@+id/content_card"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="16dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp"
app:layoutconstraintToptoBottomOf="@id/toolbar"
app:layoutconstraintBottomtoBottomOf="parent"
app:layoutconstraintStarttoStartOf="parent"
app:layoutconstraintEndtoEndOf="parent">


android:id="@+id/capture_surface"
android:layoutwidth="matchparent"
android:layoutheight="matchparent"
android:background="?attr/colorSurfaceContainerLowest" />


```

  • android/src/main/res/menu/<lower>_capture_menu.xml — toolbar action items. action_done is MANDATORY for any capture-flow operation; without it the user has no way to submit. Additional actions (Clear, Undo, etc.) per ARCHITECTURE §3.'s UI actions:

```xml

xmlns:app="http://schemas.android.com/apk/res/auto">

android:id="@+id/action_done"
android:title="@string/action_done"
app:showAsAction="always" />



```

  • For multi-mode capture operations (pen/eraser, photo/video, etc.): the toolbar / mode-selection row uses MaterialButtonToggleGroup, not plain Buttons. Toggle group provides the active-state visual feedback the user needs to know which mode is currently selected. Example layout fragment to include in activity_<lower>_capture.xml:

```xml

android:id="@+id/modetogglegroup"
android:layoutwidth="wrapcontent"
android:layoutheight="wrapcontent"
app:singleSelection="true"
app:selectionRequired="true">


Info
Category Development
Name generate-native-extension
Version v20260903
Size 72.45KB
Updated At 2026-09-06
Language