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].