Readme · Guides · Reference · Contributing
This is the native Android reference implementation for the
Contentful Optimization Android SDK. It demonstrates the
integration pattern for both Jetpack Compose (:compose) and XML Views (:views), and is the
target for the shared Maestro E2E suite (see
maestro/README.md).
OptimizationRoot initialization with mock server configurationOptimizedEntry personalization with view and click trackingScreenTrackingEffectPreviewPanelConfig preview panel with audience/variant override controlsThe app defines one locale in shared config, passes it to the native SDK as top-level locale, and
passes it directly to the raw CDA fetch helper. Entries passed to OptimizedEntry use the standard
single-locale CDA entry shape. Do not use all-locale CDA responses or locale=*, because SDK entry
resolution expects direct single-locale fields such as fields.nt_experiences and
fields.nt_variants. See
Locale handling in the Optimization SDK Suite
for the broader locale model and
Entry personalization and variant resolution
for the entry contract.
This mock app uses one Contentful locale. A production app can derive the application locale from
its own navigation, i18n, or account-preference layer and pass that value to both SDK locale and
CDA requests when they should stay aligned.
Both app shells fetch entries through Contentful's official
com.contentful.java:java-sdk CDAClient rather
than hand-rolled HTTP/JSON parsing, mirroring the iOS reference app's use of contentful.swift.
MockContentfulClient (shared/src/main/kotlin/com/contentful/optimization/shared/) builds the
shared CDAClient singleton, points it at the mock server via setEndpoint, and installs an OkHttp
interceptor (LocalesInterceptor) that synthesizes the /locales and /content_types responses
CDAClient requires before any entry fetch — the mock server only serves /entries. Content types
are served from a bundled resource (shared/src/main/resources/contentful/content_types.json,
exported by lib/mocks' fetch:ctfl:space) rather than an empty array, because contentful.java's
Rich Text resolution looks up each fetched entry's content type by ID and throws if it's missing.
ContentfulFetcher uses this client for home-screen entries (by-ID fetch, single locale,
include=10), decoding results with the SDK's typed entry APIs (CTEntry.from(CDAEntry),
OptimizedEntry(CDAEntry), OptimizedEntryView.setEntry(CDAEntry)).
MockPreviewContentfulClient wraps the same CDAClient for the preview panel's audience/experience
fetch (PreviewContentfulClient), with two adaptations specific to that content-type-filtered query
path:
nt_audience and nt_experience
fetches against the same shared CDAClient. CDAClient builds each response's resource array (and
its content-type cache) on shared instance state while a call is in flight, so two truly concurrent
calls can interleave and drop entries. MockPreviewContentfulClient serializes its calls with a
Mutex to avoid this; the built-in ContentfulHTTPPreviewClient doesn't need it because each of
its requests is an independent stateless OkHttp call.entries endpoint never
returns an includes section, so contentful.java can't resolve any linked entry and silently
drops the field entirely (not just nulls it) rather than keeping it as an unresolved link stub the
way the raw Contentful REST response would. Because entryMappers.ts only ever reads sys.id off
those fields, MockPreviewContentfulClient restores the raw, unresolved {sys: {type: "Link", ...}} value from CDAEntry.rawFields() for any field contentful.java dropped, which is enough
to unblock the audience/experience grouping the preview panel depends on.ANDROID_HOME setadb in PATHpnpm install)pnpm --filter @contentful/optimization-js-bridge buildFrom the monorepo root:
pnpm install
pnpm --filter @contentful/optimization-js-bridge build
This implementation does not use a local .env file. Mock API settings live in the Android app
configuration and point emulator traffic to the host mock server through http://10.0.2.2:8000.
The bootstrap script starts the mock server, builds the app, and launches it on an emulator:
cd implementations/android-sdk
./scripts/bootstrap.sh
Or manually:
# Terminal 1: Start mock server
pnpm serve:mocks
# Terminal 2: Build and install (the app reaches the host mock via 10.0.2.2 — no adb reverse needed)
cd implementations/android-sdk
./gradlew :compose:assembleDebug
adb install -r compose/build/outputs/apk/debug/compose-debug.apk
adb shell am start -n com.contentful.optimization.app/.MainActivity
To launch with a clean SDK state (clears the persisted profile on cold start):
adb shell am start -n com.contentful.optimization.app/.MainActivity --ez reset true
The E2E suite uses Maestro. Prefer the monorepo-root wrappers or local runner; they start the mock server, resolve or launch an emulator, build/install the app, and run the flow suite.
Run both Compose and XML Views apps:
pnpm implementation:run -- android-sdk test:e2e
Run one app shell:
pnpm implementation:run -- android-sdk test:e2e:compose
pnpm implementation:run -- android-sdk test:e2e:views
Run one Maestro suite:
pnpm implementation:run -- android-sdk test:e2e:compose -- --flow preview-panel
From implementations/android-sdk/, the equivalent Compose-only local runner is:
APP_PACKAGE=com.contentful.optimization.app ./scripts/run-e2e.sh --flow preview-panel
Omit APP_PACKAGE to run the same suite against both Compose and XML Views.
See scripts/README.md for emulator, AVD, and environment-variable details.
Open this directory (implementations/android-sdk/) as an Android Studio project. After Gradle
sync, build and launch either app on the selected device (MainActivity in :compose or :views),
set breakpoints in the app or SDK source, and run the JVM unit tests from the gutter.
Before running the app from the IDE, in a separate terminal:
# From the monorepo root, build the bridge once (or after bridge source changes):
pnpm --filter @contentful/optimization-js-bridge build
# Then start the mock server and leave it running:
pnpm --dir lib/mocks serve
The E2E suite is run from the command line rather than an IDE run configuration; see
maestro/README.md for flow structure.
Use this app when you need a debuggable native Android surface for changes in
packages/android/ContentfulOptimization or the shared JS bridge. The Gradle project includes the
SDK module from the workspace as an included Gradle subproject, so app builds compile the Kotlin
source and package the local bridge asset rather than a published AAR.
The normal loop is:
Edit Kotlin in packages/android/ContentfulOptimization/src/main/kotlin/... or bridge TypeScript
in packages/universal/optimization-js-bridge/src/....
Build the changed app or both app shells from implementations/android-sdk/:
./gradlew :compose:assembleDebug :views:assembleDebug
Run the Compose or Views app locally, then validate with the matching Maestro flow.
If bridge source changed, rebuild the bridge before treating app results as meaningful:
pnpm --filter @contentful/optimization-js-bridge build
Run the smallest check that covers the changed surface:
| Change area | Suggested validation |
|---|---|
| Bridge TypeScript only | pnpm --filter @contentful/optimization-js-bridge typecheck and pnpm --filter @contentful/optimization-js-bridge build |
| Kotlin SDK or UI adapter behavior | ./gradlew :compose:assembleDebug :views:assembleDebug |
| Compose or Views user flow | pnpm implementation:run -- android-sdk test:e2e:compose -- --flow <suite> or pnpm implementation:run -- android-sdk test:e2e:views -- --flow <suite> |
| Shared preview-panel behavior | Run the affected Maestro suite against both apps |
| Documentation-only README changes | Prettier on touched Markdown and git diff --check |
Common local pitfalls:
http://10.0.2.2:8000; no manual adb reverse setup is
required for normal local runs.