npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nuup/expo-odk-collect

v1.1.0

Published

Bridge between Expo and ODK Collect for Android

Readme

@nuup/expo-odk-collect

npm version license build status npm downloads GitHub issues GitHub pull requests

Expo module to integrate with ODK Collect for Android. Provides a type-safe bridge between your React Native / Expo app and the ODK Collect data collection app via Android Intents and ContentProviders.

Platform support: Android only.

Navigation support (current): at the moment, integration is documented and supported only with React Navigation.


Table of Contents


Installation

npx expo install @nuup/expo-odk-collect

Then run prebuild to generate native Android files:

npx expo prebuild

Requires Expo SDK 51+ and an Android device or emulator with ODK Collect installed (org.odk.collect.android).


Android Setup

The module automatically merges a <queries> declaration into your app's AndroidManifest.xml via manifest merge — no manual step needed.

If for any reason you need to add it manually, include the following inside the <manifest> tag of your android/app/src/main/AndroidManifest.xml:

<queries>
  <package android:name="org.odk.collect.android" />
</queries>

External App Setup

For your app to be launched by ODK Collect as an external app, you need two things in your app.json: a deep link scheme and the right intentFilters.

1. Configure app.json

{
  "expo": {
    "scheme": "my-app",
    "android": {
      "launchMode": "singleTask",
      "intentFilters": [
        {
          "action": "VIEW",
          "autoVerify": false,
          "data": [
            {
              "scheme": "my-app",
              "host": "*"
            }
          ],
          "category": ["BROWSABLE", "DEFAULT"]
        }
      ]
    }
  }
}

launchMode: "singleTask" — required so Android brings the existing Activity to the foreground instead of creating a new one on top. Without it, you may hit the "App entry point named main was not registered" error when ODK relaunches your app.

host: "*" — required on Android API 31+. Without the host attribute, the system ignores the intent-filter when resolving URIs that include a host component (e.g. my-app://some-screen). Using "*" matches any host under your scheme.

After editing app.json, run prebuild to regenerate the native Android files:

npx expo prebuild --platform android
# or
npx expo run:android

2. Configure the XLSForm

In your XLSForm, use android.intent.action.VIEW with uri_data to launch your app via deep link. This is the recommended approach because it creates a fresh Android Activity rather than reusing an existing one.

Single field (uses the value extra):

| type | name | appearance | |------|------|------------| | text | my_field | ex:android.intent.action.VIEW(uri_data='my-app://external-app', uuid=${instanceID}) |

Multiple fields (uses a field-list group):

| type | name | appearance | body::intent | |------|------|------------|--------------| | begin_group | my_group | field-list | android.intent.action.VIEW(uri_data='my-app://external-app', uuid=${instanceID}) | | text | field_one | | | | text | field_two | | | | end_group | | | |

Note: for the multi-field group, body::intent does not use the ex: prefix. For the single-field appearance, it does use ex:.

The field names inside the group must match exactly the keys you pass to odk.returnResult() in your app:

odk.returnResult({
  field_one: 'value A',
  field_two: 'value B',
});

3. Read the extras in your app

When ODK Collect launches your app, it passes the extras you declared in the XLSForm (e.g. uuid). Read them with odk.getIntentExtras():

import { odk } from 'expo-odk-collect';

const extras = await odk.getIntentExtras();
console.log(extras.uuid); // e.g. "uuid:550e8400-..."

How it works

This module enables your app to act as an ODK Collect external app — a pattern where ODK Collect launches your app as part of a form, waits for a result, and resumes the form with the data your app returns.

The full flow:

  1. ODK Collect opens your app via startActivityForResult. The form can pass field values as Intent extras (e.g. a UUID, a record ID, a status flag).

  2. Your app reads those extras using odk.getIntentExtras(). This lets you know which record the form is asking about.

  3. The user interacts with your app — browses a list, selects a record, fills a local form, etc.

  4. Your app calls odk.returnResult(data). This calls setResult(RESULT_OK) on the Android Activity, packs data as Intent extras, and calls finish(). ODK Collect receives the result and continues the form with the returned values.


Usage

useOdk hook

The recommended way to use the module. Returns the odk client and reactive state for results and errors.

import { useOdk } from 'expo-odk-collect';

function MyScreen() {
  const { odk, result, error } = useOdk();

  // result → last OdkActivityResult (from pickForm, pickInstance, editInstance, fillForm)
  // error  → last OdkErrorPayload from the native module
}

odk client

You can also import the odk client directly without the hook:

import { odk } from 'expo-odk-collect';

// Check installation
const installed = await odk.isInstalled();

// Detect if the Activity was opened by ODK Collect
const openedByOdk = await odk.isOpenedByOdk();

// Open ODK Collect screens
odk.launch();
odk.openForms();
odk.openInstances();

// Pick a form or instance (result arrives via onResult callback)
odk.pickForm();
odk.pickInstance();

// Edit an existing instance
odk.editInstance('42');

// Query data
const forms     = await odk.getForms();     // OdkForm[]
const instances = await odk.getInstances(); // OdkInstance[]

// Read Intent extras (when opened by ODK Collect)
const extras = await odk.getIntentExtras(); // Record<string, string>

// Return result to ODK and close the app
odk.returnResult({ field_one: 'value', field_two: 'other' });

// Subscribe to events
const sub = odk.onResult((result) => {
  console.log(result.requestCode, result.resultCode, result.uri);
});
sub.remove(); // cleanup

Full external app example:

import { useEffect, useState } from 'react';
import { Button } from 'react-native';
import { useOdk } from 'expo-odk-collect';

export default function ExternalAppScreen() {
  const { odk } = useOdk();
  const [uuid, setUuid] = useState<string | null>(null);
  const [openedByOdk, setOpenedByOdk] = useState(false);

  useEffect(() => {
    odk.isOpenedByOdk().then(setOpenedByOdk);
    odk.getIntentExtras().then((extras) => {
      const raw = extras['uuid'];
      if (raw) setUuid(raw.includes(':') ? raw.split(':')[1] : raw);
    });
  }, []);

  function handleReturn() {
    if (!openedByOdk) return; // guard: only call when opened by ODK
    odk.returnResult({
      selected_id: '99',
      selected_name: 'Site Alpha',
    });
  }

  return <Button title="Enviar a ODK" onPress={handleReturn} />;
}

Error Handling

The module emits an onError event (instead of throwing) for runtime failures. Listen to it via the useOdk hook or subscribing directly:

import { useEffect } from 'react';
import { Alert } from 'react-native';
import { useOdk } from 'expo-odk-collect';

function useOdkErrors() {
  const { error } = useOdk();

  useEffect(() => {
    if (!error) return;
    Alert.alert(`Error ODK [${error.code}]`, error.message);
  }, [error]);
}

Or directly with the odk client:

import { odk } from 'expo-odk-collect';

const sub = odk.onError((event) => {
  console.error(`[ODK] ${event.code}: ${event.message}`);
});
// sub.remove() to stop listening

Error Codes

| Code | When it fires | |------|---------------| | ODK_NOT_INSTALLED | ODK Collect is not installed on the device | | ACTIVITY_NOT_FOUND | The requested ODK Collect screen could not be opened | | QUERY_FAILED | The ContentProvider query failed | | INVALID_INTENT | The Intent could not be resolved | | UNKNOWN_ERROR | Unexpected native error |


API Reference

odk.isInstalled()

Returns Promise<boolean>true if ODK Collect is installed.

const installed = await odk.isInstalled();

odk.launch()

Opens the ODK Collect main screen.

odk.launch();

odk.isOpenedByOdk()

Returns Promise<boolean>true if the current Activity was opened by ODK Collect (detected via Activity.referrer). Use this before calling odk.returnResult() to confirm the app is running in the expected context.

const openedByOdk = await odk.isOpenedByOdk();

if (openedByOdk) {
  odk.returnResult({ selected_id: '42' });
}

Note: Activity.referrer is only populated when the Activity is started via startActivityForResult. If the user opened your app from the launcher or a deeplink, this returns false.


odk.openForms()

Opens the ODK Collect form list screen.

odk.openForms();

odk.openInstances()

Opens the ODK Collect instance (uploader) list screen.

odk.openInstances();

odk.pickForm()

Starts an ACTION_PICK intent for forms. The selected form URI is returned via the onActivityResult event (requestCode = 2001).

odk.pickForm();
odk.onResult((result) => {
  if (result.requestCode === 2001) {
    console.log('Selected form URI:', result.uri);
  }
});

odk.pickInstance()

Starts an ACTION_PICK intent for instances. The selected instance URI is returned via the onActivityResult event (requestCode = 2002).

odk.pickInstance();

odk.editInstance(instanceId)

Opens ODK Collect to edit the instance with the given ID (requestCode = 2003).

odk.editInstance('42');

odk.fillForm(formId)

Opens ODK Collect to fill a blank form with the given form ID. Use the id from getForms() (the form's row ID, not its jrFormId). On success, the created instance URI is returned via the onActivityResult event (requestCode = 2004, resultCode = RESULT_OK). If the user backs out without saving, ODK returns RESULT_CANCELED and result.uri is empty — always check resultCode before using the URI.

odk.fillForm('7');

odk.getForms()

Queries the ODK ContentProvider and returns all available forms.

const forms: OdkForm[] = await odk.getForms();
forms.forEach(f => console.log(f.displayName, f.jrFormId));

odk.getInstances()

Queries the ODK ContentProvider and returns all stored instances.

const instances: OdkInstance[] = await odk.getInstances();
instances.forEach(i => console.log(i.displayName, i.status));

odk.getIntentExtras()

Returns all Intent extras from the current Activity as a flat string map. Use this when ODK Collect opens your app to read the fields the form passed.

const extras: Record<string, string> = await odk.getIntentExtras();
// → { uuid: "uuid:abc-123", record_type: "survey", record_id: "42" }

const uuid = extras['uuid'];

odk.returnResult(data)

Sends data back to the ODK Collect form that opened your app and closes the Activity. Must be called from an Activity opened by ODK Collect.

odk.returnResult({
  selected_id: '99',
  selected_name: 'Site Alpha',
});

odk.onResult(callback)

Subscribes to activity results (from pickForm, pickInstance, editInstance, fillForm). Returns a subscription with a remove() method.

const sub = odk.onResult((result: OdkActivityResult) => {
  console.log(result.requestCode, result.resultCode, result.uri);
});

// Cleanup:
sub.remove();

odk.onError(callback)

Subscribes to errors emitted by the native module. Returns a subscription with a remove() method.

const sub = odk.onError((error: OdkErrorPayload) => {
  console.error(error.code, error.message);
});

Types

import type {
  OdkForm,
  OdkInstance,
  OdkInstanceStatus,
  OdkActivityResult,
  OdkErrorPayload,
  OdkErrorCode,
  OdkSubscription,
  OdkIntentExtras,
  OdkModuleEvents,
} from 'expo-odk-collect';

OdkForm

type OdkForm = {
  id: string;           // Internal ContentProvider row ID
  displayName: string;  // Human-readable form name
  jrFormId: string;     // JavaRosa form ID
  jrVersion?: string;   // Form version string
};

OdkInstance

type OdkInstance = {
  id: string;
  instanceId: string;    // Same as id — semantic alias
  displayName: string;
  jrFormId: string;
  jrVersion?: string;
  status: OdkInstanceStatus;
  createdAt?: string;
  deletedAt?: string;
};

type OdkInstanceStatus =
  | 'incomplete'
  | 'complete'
  | 'submitted'
  | 'submissionFailed'
  | 'unknown';

OdkActivityResult

Event payload from pickForm, pickInstance, editInstance, and fillForm.

type OdkActivityResult = {
  requestCode: number;  // 2001=pickForm, 2002=pickInstance, 2003=editInstance, 2004=fillForm
  resultCode: number;   // Android Activity.RESULT_OK (-1) or RESULT_CANCELED (0)
  uri?: string;         // URI of the selected item (if any)
};

OdkErrorPayload

type OdkErrorPayload = {
  code: OdkErrorCode;
  message: string;
  details?: string;
};

type OdkErrorCode =
  | 'ODK_NOT_INSTALLED'
  | 'ACTIVITY_NOT_FOUND'
  | 'QUERY_FAILED'
  | 'INVALID_INTENT'
  | 'UNKNOWN_ERROR';

OdkSubscription

type OdkSubscription = {
  remove: () => void;
};

AI Agent Skills

This repository includes agent skills for AI coding assistants (Claude, Copilot, etc.) that help enforce correct usage patterns automatically.

| Skill | Audience | Trigger | |-------|----------|---------| | expo-odk-collect | Contributors — working on the package itself | Editing OdkClient, types, normalizers, native module, or Kotlin code | | expo-odk-collect-integration | Consumers — integrating the package into an Expo project | Setting up app.json, building external-app screens, querying forms/instances |

What the skills cover

expo-odk-collect (contributors):

  • File architecture and responsibility of each src/ file
  • How to add a new OdkClient method end-to-end (TypeScript → Kotlin)
  • Normalizer contract for ContentProvider data
  • Error event pattern (never throw, always emit)

expo-odk-collect-integration (consumers):

  • Installation and required app.json config (launchMode, host: "*", scheme)
  • Full external-app flow with isOpenedByOdk() guard and UUID parsing
  • Querying forms and instances, subscribing to activity results
  • XLSForm configuration for single-field and multi-field groups
  • Common mistakes table

License

MIT © nuup