◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-05-05Advanced

Taking a Rork Podcast App to Production — Migrating to expo-audio, Recovering from Interruptions, and Resumable Downloads

Implementation notes for making a Rork podcast app survive real use: migrating off expo-av before SDK 55 removes it, recovering from call interruptions, resuming broken downloads, and budgeting storage against iCloud backup.

podcastbackground audio3expo-audio4expo-av6offlineRork585React Native239

✦ Premium Article

Building a podcast app sounds simple until you hit the first wall: audio that stops the moment the user locks their screen. I saw this exact issue in TestFlight feedback while building an audio wellness app — users would start playback, put the phone in their pocket, and the audio would stop within seconds. The fix turned out to be a two-line configuration change, but finding those two lines took longer than it should have.

Plan the Architecture Before Writing Code

Before generating any screens, settle on what you're building:

  • Feed management: Add RSS URLs, fetch and display episode lists
  • Playback: Background audio, seek bar, speed control (0.75x to 2x)
  • Downloads: Save episodes for offline listening
  • Lock screen: Show episode info in iOS Control Center and Android media notification

For state management, Zustand works best here. Audio is a natural singleton — any screen needs to access and control the same playback state.

# Install dependencies
npx expo install expo-av expo-file-system expo-media-library
npm install zustand react-native-xml2js date-fns

Type Definitions First

Define your data shapes upfront. This also makes prompting Rork's AI more effective — "use these TypeScript types to generate a screen" produces far more accurate code than open-ended requests.

// types/podcast.ts
 
export interface Episode {
  id: string;
  title: string;
  description: string;
  audioUrl: string;
  pubDate: Date;
  duration: number;     // seconds
  imageUrl?: string;
  localPath?: string;   // set when downloaded to device
}
 
export interface PodcastFeed {
  id: string;
  title: string;
  description: string;
  imageUrl: string;
  rssUrl: string;
  episodes: Episode[];
  lastUpdated: Date;
}
 
export interface PlayerState {
  currentEpisode: Episode | null;
  isPlaying: boolean;
  position: number;     // seconds
  duration: number;     // seconds
  playbackRate: number; // 0.75 / 1.0 / 1.25 / 1.5 / 2.0
}

The Two-Place Background Audio Configuration

The most common support request in audio apps is audio stopping when the screen locks. It always traces back to one of two missing configurations — and you need both.

Configuration 1: app.json

{
  "expo": {
    "plugins": [
      ["expo-av", { "microphonePermission": "Used for recording episode notes" }]
    ],
    "ios": {
      "infoPlist": {
        "UIBackgroundModes": ["audio"]
      }
    }
  }
}

Without UIBackgroundModes: ["audio"], iOS suspends your app's audio session the moment it backgrounds, regardless of any code-level settings. This is the declaration that tells iOS your app is a legitimate audio app.

Configuration 2: Audio Session at Runtime

// hooks/useAudioSession.ts
import { Audio } from 'expo-av';
import { useEffect } from 'react';
 
export function useAudioSession() {
  useEffect(() => {
    const initAudioSession = async () => {
      try {
        await Audio.setAudioModeAsync({
          // iOS: continue audio when app goes to background
          staysActiveInBackground: true,
          // iOS: play even in silent mode
          playsInSilentModeIOS: true,
          allowsRecordingIOS: false,
          // Android: fully stop other audio rather than ducking
          shouldDuckAndroid: false,
          interruptionModeAndroid: Audio.INTERRUPTION_MODE_ANDROID_DO_NOT_MIX,
          interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX,
        });
      } catch (error) {
        // Audio session failure shouldn't crash the app — log and continue
        console.error('Failed to initialize audio session:', error);
      }
    };
 
    initAudioSession();
  }, []);
}

Call this hook exactly once in your root layout (app/_layout.tsx). Calling it before every playback resets the iOS audio session and can cause other apps' audio to unexpectedly resume.

On shouldDuckAndroid: false: setting this to true means a phone call will just lower your volume rather than pausing playback. For podcasts, pausing on interruption is better UX — users want to catch what was said after a call, not miss it.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦A mapping table for migrating off expo-av before SDK 55 removes it, including the known Android limitation on interruptionMode
✦Resumable downloads built on resumeData — why file existence is the wrong completion check and how HTTP 206 breaks naive code
✦Surviving three kinds of interruption (calls, unplugged headphones, storage pressure) plus five one-minute hardware checks
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⬡ Dev Tools2026-08-31
Three Minutes After the Screen Locked, My expo-audio Ambient Sound Went Silent
Why expo-audio stops playing when the screen locks, diagnosed as three separate layers: build config, audio session, and lock screen integration. The three-minute stop on Android is documented behavior.
⬡ Dev Tools2026-09-04
EAS secret visibility does not keep a value out of your app — deciding prefix and visibility separately
The EXPO_PUBLIC_ prefix decides what ships inside your app; EAS visibility decides who can read it. Why stacking them blanks a value on OTA updates, and how to check your build.
⬡ Dev Tools2026-08-22
Every bulk replace exited zero. The damage was in the lines I did not delete
Run a bulk replace over generated code and the breakage lands on the neighbouring lines, not the matched ones. Here is what broke in a live project, and a dependency-free guard that checks the invariants a replace must preserve.