Cross-platform native music apps with Expo

iOSAndroidExpoaudio
6 Dec 2023

A guide to developing audio focused mobile applications for multiple platforms using React Native and Expo.

Expo

Install basic codebase

npx create-expo-app@latest --template tabs@49

Local development

npx expo run:ios

Install expo-dev-client

npx expo install expo-dev-client

Now trying to follow Setup Code Signing Certificates in Xcode for Development

Note: this requires a paid Developer account

--- more to add here once working

Updating to latest version of Expo

Version 50

Expo SDK 50 beta is now available

npm install expo@next
npx expo install --fix

This caused an error

⚠️  Something went wrong running `pod install` in the `ios` directory.
Command `pod install --repo-update` failed.
└─ Cause: This is often due to native package versions mismatching. Try deleting the 'ios/Pods' folder or the 'ios/Podfile.lock' file and running 'npx pod-install' to resolve.

So I nuked the iOS directory and ran again

npx expo run ios

A new error

iOS Bundling failed 2392ms (index.js)
error: index.js: [BABEL]: expo-router/babel is deprecated in favor of babel-preset-expo in SDK 50. (While processing: /Users/chrismasters/git/expo-audio/node_modules/expo-router/babel.js)

This article was useful Expo Router v3 beta is now available

Remove the expo-router/babel preset in favor of babel-preset-expo and be sure to clear the Metro cache before restarting your dev server—this means running npx expo start --clear or npx expo export --clear

module.exports = function (api) {
  api.cache(true);
  return {
    presets: ['babel-preset-expo']
  };
};

Then there was an issue with the certificate which required following these instructions

Final issue was connectin to the dev server.

Would not connect whilst on work VPN, it also required connecting to the dev server URL manually via the IP address ie

http://192.168.1.16:8081

After that the build was successful.

React Native Track Player

You can now use React Native Track Player with Expo.

Please be aware that while many people are using React Native Track Player with Expo successfully, the current maintainers of this project do not use Expo and their ability to resolve issues involving Expo is limited.

To get started, create a custom development client for your Expo app and then install React Native Track Player.

npx expo install react-native-track-player

Custom index.js file

Remove from package.json

"main": "expo-router/entry"

Then create an index.js file in the root (Taken from Expo Router Troubleshooting).

Alternatively, you can circumvent this issue by creating an index.js file in the root of your project with the following contents:

import { registerRootComponent } from 'expo';
import { ExpoRoot } from 'expo-router';
import TrackPlayer from 'react-native-track-player';

// Must be exported or Fast Refresh won't update the context
export function App() {
  const ctx = require.context('./app');
  return <ExpoRoot context={ctx} />;
}

TrackPlayer.registerPlaybackService(() => require('./service'));
registerRootComponent(App);
import { registerRootComponent } from 'expo';
import { ExpoRoot } from 'expo-router';
import TrackPlayer from 'react-native-track-player';

// Must be exported or Fast Refresh won't update the context
export function App() {
  const ctx = require.context('./app');
  return <ExpoRoot context={ctx} />;
}

TrackPlayer.registerPlaybackService(() => require('./service'));
registerRootComponent(App);

And service.js file

import TrackPlayer, { Event } from 'react-native-track-player';

module.exports = async function () {
  TrackPlayer.addEventListener(Event.RemotePlay, () => TrackPlayer.play());
  TrackPlayer.addEventListener(Event.RemotePause, () => TrackPlayer.pause());
  TrackPlayer.addEventListener(Event.RemoteStop, () => TrackPlayer.stop());
};
import TrackPlayer, { Event } from 'react-native-track-player';

module.exports = async function () {
  TrackPlayer.addEventListener(Event.RemotePlay, () => TrackPlayer.play());
  TrackPlayer.addEventListener(Event.RemotePause, () => TrackPlayer.pause());
  TrackPlayer.addEventListener(Event.RemoteStop, () => TrackPlayer.stop());
};

So this doesn't seem to work, better to look at the demo application where the service is directly imported ?

PlayPauseButton.tsx SetupService.ts

import { QueueInitialTracksService, SetupService } from './services';

function useSetupPlayer() {
  const [playerReady, setPlayerReady] = useState<boolean>(false);

  useEffect(() => {
    let unmounted = false;
    (async () => {
      await SetupService();
      if (unmounted) return;
      setPlayerReady(true);
      const queue = await TrackPlayer.getQueue();
      if (unmounted) return;
      if (queue.length <= 0) {
        await QueueInitialTracksService();
      }
    })();
    return () => {
      unmounted = true;
    };
  }, []);
  return playerReady;
}

Following the guides

Resources

Libraries

Expo