I burned four EAS cloud builds and two hours chasing crashes that had nothing to do with my code. All three bugs came from Expo SDK 56 defaults that silently break Android builds. Here's each one and how to fix it.
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.
Top comments (0)