skip to content

Device Capability APIs

The most-used SDK packages wrap camera, location, images, media picking and haptics in async APIs. Interviewers ask which need a config plugin and what differs between iOS and Android.

part ofExpo (React Native)overview, primer and where to startread it →
on this pageshow

explore

questions

5

In an Expo app, how do you take a photo with expo-camera's CameraView, and what must the code wait for before capturing?

level: juniorimportance: must knowfreq 50%

answer

  1. a ref to the camera component
  2. permission first, preview second
  3. onCameraReady before capturing
  4. takePictureAsync returns a temporary uri
  5. one active preview at a time

basics

~20 s

Render CameraView once camera permission is granted, keep a ref, wait for onCameraReady, then call ref.current.takePictureAsync(options). It resolves with a uri to a temporary cache file, so copy it somewhere permanent before relying on it.

solid answer

~40 s

`CameraView` from `expo-camera` renders the live preview; `facing` defaults to `'back'` and `flash` to `'off'`. I gate it on `useCameraPermissions`, hold a ref, and enable the capture button only after `onCameraReady` fires, because the docs say to wait for it before calling `takePictureAsync`. The call accepts options such as `quality` (0 to 1, default 1), `base64`, `exif` and `skipProcessing`, and resolves with `uri`, `width` and `height`. The `uri` points to a temporary file in the app's cache, so a receipt must be copied to permanent storage. Only one camera preview can be active at a time, so the `CameraView` should unmount when its screen loses focus. On iOS the `active` prop can also stop the session without unmounting.

code

tsx · 27 lines
tsx
import { CameraView, useCameraPermissions } from 'expo-camera';
import { useRef, useState } from 'react';
import { Button, View } from 'react-native';

export function ReceiptCamera({ onCaptured }: { onCaptured: (uri: string) => void }) {
  const [permission, requestPermission] = useCameraPermissions();
  const camera = useRef<CameraView>(null);
  const [ready, setReady] = useState(false);

  if (!permission) return null;
  if (!permission.granted) {
    return <Button title="Allow camera to scan receipts" onPress={requestPermission} />;
  }

  const capture = async () => {
    const photo = await camera.current?.takePictureAsync({ quality: 0.7 });
    // photo.uri is a temporary cache file: copy it before saving the receipt.
    if (photo) onCaptured(photo.uri);
  };

  return (
    <View style={{ flex: 1 }}>
      <CameraView ref={camera} style={{ flex: 1 }} facing="back" onCameraReady={() => setReady(true)} />
      <Button title="Capture receipt" disabled={!ready} onPress={capture} />
    </View>
  );
}

go deeper

for a junior

Recall the sequence: permission, a ref to CameraView, wait for onCameraReady, then takePictureAsync, and that the returned uri is temporary.

for a middle

Explain the capture options, quality, base64, exif and skipProcessing, and why only one preview can be active at a time.

for a senior

Diagnose black previews and failed captures across navigators and platforms, and decide which native options, such as barcode scanning or audio permission, a receipt app should drop.

for a principal

Weigh an in-app camera against the system camera UI for a capture flow, considering control, permissions, app size and maintenance.

## The component and its defaults `expo-camera` exports **`CameraView`**, a native view that shows the live camera preview and exposes capture methods through a **ref**. The props that matter for a receipt scanner: | prop | default | purpose | |---|---|---| | `facing` | `'back'` | which camera, `'front'` or `'back'` | | `flash` | `'off'` | `'on'`, `'off'` or `'auto'` for the still photo | | `enableTorch` | `false` | keeps the light on, useful for dim receipts | | `mode` | `'picture'` | `'picture'` or `'video'` | | `zoom` | `0` | 0 to 1 of the device's maximum zoom | | `active` | `true` | iOS only: stop the session without unmounting | | `onCameraReady` | none | fires when the preview has started | | `onMountError` | none | fires when the preview could not start | ## The capture sequence 1. **Permission**: read and request camera access with `useCameraPermissions` from `expo-camera`; render the preview only once it is granted. 2. **Ref**: keep a ref to the `CameraView` instance; capture methods live on it. 3. **Ready**: wait for **`onCameraReady`**. The source documents this explicitly for `takePictureAsync`: calling earlier risks a failed or empty capture. 4. **Capture**: `await ref.current.takePictureAsync(options)`. 5. **Persist**: move the result out of the cache before relying on it. ## What takePictureAsync takes and returns Options include: - **`quality`**: 0 to 1, default **1**; lower values shrink the file, which suits receipts that only need to be legible. - **`base64`** and **`exif`**: also return the JPEG as Base64 or the EXIF tags. - **`skipProcessing`**: skips orientation correction and the processing pipeline, so it is faster but can leave the photo rotated, and `quality` is ignored. - **`shutterSound`**: default `true`; set `false` to silence it. The promise resolves with **`uri`**, **`width`**, **`height`** and the optional extras. On native platforms the **`uri` is a temporary file in the app's cache directory**; the docs recommend copying it with the file-system package to keep it. Two timing details from the source: - Calling `takePictureAsync` **while the preview is paused** throws on Android and returns the last on-screen frame on iOS. - Passing `onPictureSaved` makes the promise resolve immediately, with the picture delivered to that callback once saved. ## One preview at a time The docs warn that **only one camera preview can be active at any given time**. In a tabbed or stacked app, a camera screen that stays mounted in the background can leave the next camera screen black. The fix is to render `CameraView` only while its screen is focused. On iOS, the `active` prop can additionally stop the session while the view stays mounted. ## Reading a code printed on the receipt Many receipts carry a QR code. `CameraView` can read it during the preview: - pass **`barcodeScannerSettings={{ barcodeTypes: ['qr'] }}`** to limit the types scanned; - handle **`onBarcodeScanned`**, which receives the code's `type` and `data`; - guard against repeats: the component drops an identical result only if it arrives within 500 ms of the previous one, so the same code keeps firing while it stays in frame. Set a flag, or stop passing `onBarcodeScanned`, after the first read. Scanning relies on the barcode support that the `barcodeScannerEnabled` plugin option controls. ## Native configuration `expo-camera` has a config plugin whose options become part of the native build: - **`recordAudioAndroid`**, default `true`, adds Android's audio recording permission for video; a photo-only receipt scanner can set it to `false`. - **`barcodeScannerEnabled`**, default `true`, includes barcode scanning support; setting it to `false` reduces app size when the app never reads codes. - The iOS camera and microphone prompt texts are set through the same plugin. Changing any of them requires a new native build. ## Common mistakes - Calling `takePictureAsync` from a `useEffect` on mount, before `onCameraReady`. - Storing the returned `uri` in the database as if it were permanent. - Leaving a camera screen mounted under a navigator while another camera screen opens.

  • Why can a second camera screen show a black preview in an Expo app with tabs?
    Only one camera preview can be active at a time. If the first screen's `CameraView` stays mounted in an inactive tab, it can hold the camera. Render the `CameraView` only while its screen is focused, and on iOS the `active` prop can stop the session without unmounting.
  • When would you set skipProcessing: true in takePictureAsync?
    When capture latency matters more than orientation, for example for a quick throwaway frame. It skips the processing pipeline, so the photo can come back rotated on some devices and `quality` is ignored. For receipts that must be read later, keep processing on.

saying these in an interview costs you the question

  • takePictureAsync can be called as soon as the component mounts.
  • The uri returned by takePictureAsync is a permanent file.
  • Several CameraView previews can run at once on different screens.
  • CameraView's active prop works the same on Android and iOS.
  • skipProcessing only makes capture faster with no side effects.
open as a page

In expo-image, what do the cachePolicy and placeholder props control, and how would you use them for a list of receipt thumbnails?

level: middleimportance: should knowfreq 38%

basics

~20 s

cachePolicy chooses where expo-image caches a loaded image: 'none', 'disk' (the default), 'memory' or 'memory-disk'. placeholder shows something, such as a blurhash, until the real image loads. For signed receipt URLs, set a stable cacheKey too.

open as a page

With expo-image-picker, what does launchImageLibraryAsync return, and how should a receipt-import flow handle the result?

level: middleimportance: should knowfreq 40%

basics

~20 s

launchImageLibraryAsync opens the system picker and resolves with { canceled, assets }. When canceled is false, assets holds objects with uri, width, height and file details; a cancel is a normal outcome, not an error.

open as a page

In expo-location, when do you use getCurrentPositionAsync versus watchPositionAsync, and what does each cost in time and battery?

level: middleimportance: should knowfreq 40%

basics

~20 s

getCurrentPositionAsync asks for one fresh fix, which can take seconds; watchPositionAsync streams updates to a callback until you call remove() on its subscription, and only while the app is in the foreground. getLastKnownPositionAsync is the fast, possibly stale option.

open as a page

In an Expo receipt-scanning app, why might expo-haptics feedback on capture work on Android but do nothing on iOS?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

On iOS the Taptic Engine does nothing while the camera is active, in Low Power Mode, during dictation, or when the user disabled it, and the call still resolves. Android simulates impacts with the Vibrator service, which has no such documented rule.

open as a page