BXUP Track progress & test yourself — free
Frontend & Mobile · Hands-on lab · ~5 min read
FRONTEND & MOBILE · HANDS-ON LAB

Lab 15 · Mobile Capstone — On-Device AI Feature

Ship one on-device AI feature on your platform — Android ML Kit GenAI, iOS Foundation Models, cross-platform, or Chrome built-in AI for web-only learners — with a fallback path for unsupported devices that you prove actually executes. A convincing demo isn't enough; the fallback branch must be shown running.

Wei is a mobile engineer whose PM wants "summarize this thread with AI, on-device, private, free." The demo works beautifully on Wei's flagship test phone. Then QA runs it on a three-year-old mid-range device with no on-device model support and the feature crashes — because the happy-path demo was the only path anyone built. On-device AI is a capability-detection problem before it's an AI problem: the model may or may not be present, downloading, or supported, and the graceful-degradation path is the actual deliverable. This capstone holds you to proving the fallback runs, not just the feature.

Time & tier

~120 minutes hands-on · Go Mobile stage (maps to module M8, playbook PB-52) · $0 — pick the track matching your platform; the web track needs only Chrome Canary and runs on any machine, so it's the universal fallback if you lack mobile tooling.

Setup

Pick one track. All four build the same feature — summarize a text thread on-device with a cloud-or-stub fallback — and all four are graded against the same rubric.

Track W — Chrome built-in AI (web, universal). Chrome's Summarizer/LanguageModel APIs run Gemini Nano locally.

npm create vite@latest ondevice-lab -- --template vanilla-ts
cd ondevice-lab && npm install
# Chrome Canary/Dev with the on-device AI flags enabled (chrome://flags → "Prompt API"/"Summarization API")

src/summarize.ts — the capability-gated core (author the UI around it):

export type SummarizeResult = { text: string; source: "on-device" | "cloud" | "unavailable" };

export async function summarize(thread: string): Promise<SummarizeResult> {
  // 1. Capability detection — the heart of the lab.
  const canDevice = "Summarizer" in self &&
    (await (self as any).Summarizer.availability?.()) !== "unavailable";

  if (canDevice) {
    const avail = await (self as any).Summarizer.availability();
    if (avail === "downloadable" || avail === "downloading") {
      // model not ready yet — surface progress, then this call falls through to cloud
      return cloudFallback(thread, "model downloading");
    }
    const summarizer = await (self as any).Summarizer.create({ type: "tldr", length: "short" });
    const text = await summarizer.summarize(thread);
    return { text, source: "on-device" };
  }
  return cloudFallback(thread, "no on-device support");
}

async function cloudFallback(thread: string, _why: string): Promise<SummarizeResult> {
  // Stub: in production this is your BFF proxy (see Lab 13). For the lab, a deterministic
  // extractive summary proves the branch runs without needing a provider key.
  const firstLines = thread.split("\n").filter(Boolean).slice(0, 2).join(" ");
  return { text: `(cloud) ${firstLines}…`, source: "cloud" };
}

Track A — Android ML Kit GenAI (Kotlin/Compose). Use ML Kit GenAI Summarization; capability check via the feature's checkFeatureStatus/download API; fallback to a cloud call (Firebase AI Logic) or a stub. Scaffold with Android Studio's Gemini/Agent Mode, but you own the fallback logic.

Track I — iOS Foundation Models (Swift/SwiftUI, iOS 26 / Xcode 26). Use the Foundation Models framework; gate on SystemLanguageModel.default.availability (.available vs .unavailable(reason)); fallback to cloud/stub when unavailable (older device, Apple Intelligence off, model not ready).

Track X — Cross-platform (Flutter AI Toolkit or react-native-ai). Detect the on-device provider; degrade to a cloud provider behind App Check when absent.

The evidence ledger, ondevice-report.md:

| Item | Evidence |
|------|----------|
| On-device path runs | (recording timestamp / log line showing source="on-device") |
| Capability detection branches | (the availability values you handled) |
| Fallback path RUNS (not just exists) | (recording/log showing source="cloud" reached) |
| Feature correctness both paths | (summary quality note) |
| Privacy/cost rationale | (why on-device here) |

Guided steps

  1. Build the happy path first. Wire the UI: a textarea/field prefilled with a 10–15 line sample thread, a Summarize button, a result area that shows the summary and a visible badge of its source ("on-device" / "cloud"). Run on a supported device/browser. Expected: a summary with the on-device badge. This is the easy third.
  2. Enumerate the capability states. In ondevice-report.md, list every availability value your platform exposes — Chrome: unavailable | downloadable | downloading | available; iOS: .available | .unavailable(deviceNotEligible | appleIntelligenceNotEnabled | modelNotReady); Android/ML Kit: feature-status + download states. Each is a branch you must handle. A feature that handles only "available" is the QA-crash from the scenario.
  3. Handle the not-ready-yet states. For downloadable/downloading/modelNotReady: the model could work later but isn't ready now. Design this explicitly — either trigger the download with visible progress and let this request use the fallback, or surface "preparing on-device AI…". Do not treat not-ready as permanently-unavailable, and do not block the user. Log which choice you made and why.
  4. Force the fallback and prove it runs. This is the crux. Simulate an unsupported device: on web, disable the Chrome flag or add a ?forceUnavailable=1 switch that short-circuits canDevice to false; on iOS, run the older-device simulator or an availability-mocking flag; on Android, a build flag that reports the feature unavailable. Run the feature. Expected: the summary still appears, badged "cloud", and a log line proves cloudFallback executed. Record this — an unexecuted fallback branch counts as not built.
  5. Verify correctness on both paths. The fallback summary needn't match the on-device one, but it must be a real, useful result — not an error, not empty, not "AI unavailable." Compare the two outputs for your sample thread and note quality in the ledger. A fallback that degrades to unusable is a fallback in name only.
  6. Add the privacy/cost rationale. Two paragraphs in the report: why on-device is the right default here (the thread is private user content; on-device keeps it local, costs nothing, works offline), and what the fallback trades away (cloud sees the content → needs the Lab 13 proxy + consent; costs money; needs network). This is the deliberate on-device-vs-cloud choice the module teaches, made explicit.
  7. Record the proof. A screen recording (or paired screenshots + logs) showing, in one session: (a) the on-device path producing a summary with its badge, and (b) the forced-fallback path producing a summary with the cloud badge. Two branches, both observed. Save to proof/.
  8. Write the degradation contract. Ten lines: the capability states, what the UI does in each, the fallback trigger, and the privacy posture. This is what you'd put in the feature's tech spec so QA knows exactly which device conditions to test.
Warning

"It works on my device" is the on-device AI trap in one sentence. Model availability depends on device tier, OS version, user settings (Apple Intelligence toggle), storage, and download state — none of which your test phone represents. The fallback branch is not a nice-to-have; on the installed base it is the path a large fraction of users will actually hit.

Verify

  • The feature produces a correct summary on the supported path, badged on-device.
  • Capability detection handles every availability state your platform exposes — not just the ready one — with the not-ready state designed, not crashed.
  • The fallback path is proven to execute via the forced-unavailable run, badged cloud, with a log line as evidence — existence in code is insufficient.
  • Both paths produce a usable summary of the sample thread.
  • proof/ contains a recording showing both branches in one session.
  • ondevice-report.md has all five ledger rows filled and the privacy/cost rationale.

Self-grading rubric (pass = 7/10)

  • On-device feature works (0–2): correct summary, source badge accurate = 2.
  • Capability coverage (0–3): all availability states branched, not-ready handled distinctly from unavailable = 3.
  • Fallback proven running (0–3): forced-unavailable run recorded with the cloud branch observably executing = 3; branch exists but never demonstrated = 0.
  • Deliberate choice (0–2): privacy/cost rationale and degradation contract articulate the on-device-vs-cloud tradeoff = 2.
▸Answer key — what "graceful degradation, proven" requiresoptional deep dive — click to expand

This capstone has no planted bug; it grades the discipline the scenario's QA crash exposes. The reference solution demonstrates all of the following, and the most common failure is skipping the last one:

  • Capability detection is the feature. Every platform gates on an availability signal with three-plus states, not a boolean: supported-and-ready, supported-but-not-ready (downloadable/downloading/modelNotReady), and unsupported (device ineligible, OS too old, user setting off). Correct solutions branch all three. The classic defect — the one that produces the scenario's crash — is if (supported) { use it } else { error }, which both crashes the not-ready case and offers no real fallback for the unsupported case.
  • Not-ready is a distinct, designed state. A downloadable model is not an unavailable model. The reference either kicks off the download with progress and serves this request via fallback, or shows a "preparing" state — never blocks, never treats it as permanent failure. Grading rewards handling this state distinctly.
  • The fallback must be observed executing. The entire lesson is that a fallback branch nobody has run is indistinguishable from a bug. Full credit is gated on the step-4 forced-unavailable run being recorded, with the cloud branch's log line or badge visible. A code path that "would run if a device were unsupported" but was never triggered scores zero on that row — because that is exactly the state the scenario's team was in when QA's phone crashed.
  • Both paths are actually usable. The fallback produces a real summary (cloud or a deterministic extractive stub), not an "unavailable" message. A degradation to uselessness is a failure disguised as handling.
  • The choice is deliberate and documented. On-device is chosen here for privacy (thread content stays local), cost (free), and offline capability; the fallback consciously trades privacy and money for coverage and must therefore inherit the Lab 13 proxy/consent posture. Stating this tradeoff — rather than defaulting to on-device because it's trendy or cloud because it's easy — is the judgment the module certifies.

Platform-specific correctness notes: iOS SystemLanguageModel.default.availability returns .unavailable(reason) with an enum you should switch on; Chrome's availability() returns the four-value string enum and create() accepts a download-progress monitor; ML Kit GenAI exposes feature status + a download flow. Whichever track you took, the report must name the actual availability values your platform returned, not generic placeholders.

Key Takeaway:

On-device AI is a capability-detection problem wearing an AI costume — the model's presence depends on device tier, OS, settings, and download state your test hardware never represents, so the deliverable isn't the summary, it's the proven-executing fallback path; a degradation branch you haven't watched run is the crash QA is about to find.

This is the free reading view. Open BXUP to track your progress, run the hands-on labs, and prove it with original, exam-pattern assessments.
© Anvi Innovate · BXUP — Be Exponentially Upskilled · BXUP is an independent learning platform, not affiliated with or endorsed by AWS, Microsoft, Google, NVIDIA, IBM, Salesforce, Databricks, Oracle or Anthropic. All trademarks belong to their respective owners. · bxup.in