Bug 1: expo-av crashes with NoClassDefFoundError

I added voice recording to a dream journal app. The Expo docs for Audio still reference expo-av in some examples. So I installed it:

npx expo install expo-av
      

The app compiled. TypeScript was happy. Then the EAS build failed on Android with:

java.lang.NoClassDefFoundError: 
        Failed resolution of: Lio/expo/modules/video/VideoViewModel;
      

The expo-av package pulls in video dependencies. In SDK 56, the video module was extracted to a separate expo-video package. The old monolith references classes that no longer exist.

The fix: expo-av is deprecated starting SDK 55. Use expo-audio for audio and expo-video for video. They're separate packages now.

npm uninstall expo-av
      npx expo install expo-audio
      

The API changed too. Old:

import { Audio } from 'expo-av';

      const recording = new Audio.Recording();
      await recording.prepareToRecordAsync(Audio.RecordingOptionsPresets.HIGH_QUALITY);
      await recording.startAsync();
      

New:

import { useAudioRecorder, RecordingPresets } from 'expo-audio';

      const recorder = useAudioRecorder(RecordingPresets.HIGH_QUALITY);
      recorder.record();
      

The new API is hook-based. No more class instances, no manual cleanup. useAudioRecorder handles permissions, lifecycle, and cleanup on unmount.

Time lost: 4 EAS builds (~60 minutes). The error message mentions VideoViewModel, which sent me down a wrong path investigating video dependencies before I realized the entire package was deprecated.

Bug 2: Gradle 9.x silently breaks React Native

After fixing the audio crash, the next build failed with a different NoClassDefFoundError:

java.lang.NoClassDefFoundError: 
        com/android/build/api/variant/impl/JvmVendorSpec
      

npx expo prebuild generated gradle-wrapper.properties pointing to Gradle 9.3.1. Gradle 9 removed JvmVendorSpec.IBM_SEMERU, which React Native's Gradle plugin still references internally.

The error doesn't mention Gradle versions. It doesn't say "incompatible Gradle." It just throws a class-not-found at build time.

The fix: Pin Gradle to 8.x. After every npx expo prebuild, check the generated wrapper:

# Check what version prebuild generated
      grep distributionUrl android/gradle/wrapper/gradle-wrapper.properties
      

If it says anything starting with gradle-9, change it:

distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
      

Add this to CI to catch it automatically:

GRADLE_VER=$(grep -oP 'gradle-\K[0-9]+' android/gradle/wrapper/gradle-wrapper.properties)
      if [ "$GRADLE_VER" -ge 9 ]; then
        echo "ERROR: Gradle $GRADLE_VER breaks React Native. Pin to 8.x"
        exit 1
      fi
      

Time lost: 2 builds. The error looks identical to the expo-av crash (both are NoClassDefFoundError), which made me think I hadn't fully fixed bug #1.

Bug 3: Barrel exports + native modules = cascading crash

I had a standard barrel export file:

// src/dream/components/index.ts
      export { DreamCard } from './DreamCard';
      export { MoodPicker } from './MoodPicker';
      export { VoiceRecorder } from './VoiceRecorder';
      

VoiceRecorder imports expo-audio. Every screen that imported anything from @dream/components would trigger the native module resolution for expo-audio, even screens that never rendered the recorder.

In Expo Go (no native modules bundled), this crashes the entire app. Not just the recording screen. Every screen.

The fix: Never barrel-export components that depend on native modules. Import them directly and lazy-load:

// src/dream/components/index.ts
      export { DreamCard } from './DreamCard';
      export { MoodPicker } from './MoodPicker';
      // VoiceRecorder NOT barrel-exported -- requires native module
      // Import directly: import { VoiceRecorder } from '@dream/components/VoiceRecorder'
      

On the consuming screen, use React.lazy:

import { lazy, Suspense } from 'react';

      const VoiceRecorder = lazy(() =>
        import('@dream/components/VoiceRecorder')
          .then(m => ({ default: m.VoiceRecorder }))
      );

      // In render:
      <Suspense fallback={<ActivityIndicator />}>
        <VoiceRecorder />
      </Suspense>
      

This way the native module only loads when the component actually renders, and screens that don't use it never touch expo-audio.

Time lost: 1 hour. The crash logs pointed to the native module, not the import chain. I kept looking at expo-audio configuration when the real problem was in index.ts.

The checklist I wish I had

Before your next Expo SDK 56 Android build: grep for expo-av (replace with expo-audio/expo-video), check gradle-wrapper.properties isn't 9.x after prebuild, and audit barrel exports for native module imports.

But mostly: check the SDK changelog before choosing packages. I would have caught bug #1 in 30 seconds by reading the Expo SDK 56 changelog. The deprecation is documented. I just didn't look.


I write these from real work at astraedus.dev, that's where I build apps and tools. Building something, or stuck on something like this? Reach me at astraedus.dev or [email protected].