Ejecting from Expo

A detailed guide on ejecting from Expo, fixing all problems that follow, and building a release bundle.

Ejecting from Expo

A detailed guide on ejecting from Expo, fixing all problems that follow, and building a release bundle.

After a week-ish of testing the app, I decided it was time to eject from the managed workflow on Expo and switch to the bare version of things so I could start implementing the in-app payments. I am also hoping this results in a reduced app size but let's see how that goes.

Unfortunately, several Google searches later, I've realized there isn't a lot of documentation around this. So I thought, why not just hit expo eject and figure it out as we go?

I'll try and post every problem I run into and how I eventually solved it and link to whatever was helpful in doing so, because your solution might not exactly be the same as mine. Also, this is week 3(?) in React Native for me so you may notice something I didn't. Alright, enough talk, let's go.


Result of expo eject

The image above is what we see once we eject. It shows a bunch of warnings for both Android and iOS. At the moment, I am only worrying about Android but I am going to go ahead and ignore those warnings and just try running the app and seeing how it goes.

Splash Screen

Splash Screen Error

Well, that didn't take long, did it? To be fair to Expo, it already warned us about this. Upon some digging, I found out that AppLoading is an API that isn't available in the bare workflow. So to fix this, we are going to have to use the expo-splash-screen package. Let's start by installing it.

yarn add expo-splash-screen

Next, we need to get rid of all our code that uses the AppLoading component from Expo and use our newly installed package to do the same thing. You can see some examples here and also what I ended up with below.

import React, { useState, useEffect } from 'react';
import { useFonts } from '@use-expo/font';
import * as SplashScreen from 'expo-splash-screen';

const App = () => {
  const [isReady, setIsReady] = useState(false)
  const [isLoaded] = useFonts({
    'Poppins-Regular': require('./assets/fonts/Poppins-Regular.ttf'),
    'Poppins-Medium': require('./assets/fonts/Poppins-Medium.ttf'),
    'Poppins-SemiBold': require('./assets/fonts/Poppins-SemiBold.ttf'),
  });

  useEffect(() => {
  	// Stop the Splash Screen from being hidden.
    const showSplashScreen = async () => {
      await SplashScreen.preventAutoHideAsync();
    }
    showSplashScreen();
    // You can do additional data fetching here.
    // I have a function that fetches my user from Firebase
    // but I have left it out because it is kind of irrelevant
    // in this demo.
  }, []);

  useEffect(() => {
  	// Once our data is ready, hide the Splash Screen
    const hideSplashScreen = async () => {
      await SplashScreen.hideAsync();
    }

    if (isLoaded && isReady) hideSplashScreen();
  }, [isReady])
  
  if (!isReady) return null;
  
  return (
    <RootComponent />
  )
}
  
Splash Screen Configuration

Next, we need to hook into the native view hierarchy and tell it about our Splash Screen. There is an Automatic Configuration section in the README but that did not work for me and I had to configure it manually using the steps provided in the Manual Configuration.

If you also went for the automatic configuration and had it fail for you, you'll probably have to delete res/values/colors_splashscreen.xml and res/values/styles_splashscreen.xml as they are just empty files. Again, if the automatic thing works for you, great. If it doesn't, this might be something you need to fix or yarn android will keep failing.

The docs are pretty on point as once I followed them and restarted my server, I had the Splash Screen showing up and my app running as expected!

Push Notifications

If you're using the push notifications setup that comes with the managed version of Expo, that's probably going to break as well. Luckily, since SDK 37, it also works with the bare workflow. Let's start by installing this package.

expo install expo-notifications

Next, we need to change how we get the push token from our user. In the managed workflow, Expo has access to the manifest and picks up your ID from there. But in the bare workflow, we have to pass it manually.

import Constants from 'expo-constants';
import * as Notifications from 'expo-notifications';

let experienceId = undefined;
// This was a little confusing for me from the docs
// Your experience ID is basically your Expo username followed by
// the slug for the app you need the tokens for.
if (!Constants.manifest) experienceId = '@username/slug';

// Another thing to note here is that the token is actually under
// the data key. This is different from the managed workflow where
// this method would directly return the token.
const token = await Notifications.getExpoPushTokenAsync({ experienceId });

// Basically a function that updates the token in your DB
await updatePushToken(userId, token.data);
Updating Expo Push Token

Update your push token fetching code wherever applicable and now your app should be updating the push tokens as it was before and everything should be working fine. You can also test this by using the notification testing tool that Expo provides.

App Icons

While testing my notifications set up, I noticed that the app icon was the default Android icon, which means our icon setup is broken as well. At this point, we should have predicted this as the CLI even warned us about it. You know, in the warnings we chose to ignore. To fix this, I followed this guide on the official developer documentation for Android apps.

Basically, open Android Studio, go for a run, or something because it takes forever to read and understand your project, look for the res folder under app/src/main, right-click it, and click New -> Image Asset. This opens the handy little wizard and all you have to do is follow the guide linked above and choose the icon files that you used in your app.json file earlier with Expo.

Once you follow the guide, just uninstall and reinstall the app on your device/simulator and you should see your actual icon!

Keyboard

Another issue that randomly popped up for me was that my keyboard was suddenly covering all my inputs. This was surprising because this seemed to work perfectly fine when I ran my app with Expo.

To fix this, I simply had to wrap my view with the KeyboardAvoidingView component from React Native. Come to think of it, I probably should have been using it already. Here's a small snippet of what needs to be done.

import { KeyboardAvoidingView } from 'react-native';

<KeyboardAvoidingView behavior="padding">
	// Existing UI code
</KeyboardAvoidingView>

Building

This is the part you're probably here for, right? So let's get started.

To begin with, we need to fetch our existing keystore from Expo.

expo fetch:android:keystore

This should show you your Keystore password, Key alias, Key password and, also create a .jks file in your root directory.

Next, open android/gradle.properties and set up your Gradle variables.

MYAPP_UPLOAD_STORE_FILE=KEYSTORE_FILE
MYAPP_UPLOAD_KEY_ALIAS=KEY_ALIAS_FROM_EXPO
MYAPP_UPLOAD_STORE_PASSWORD=KEYSTORE_PASSWORD_FROM_EXPO
MYAPP_UPLOAD_KEY_PASSWORD=KEY_PASSWORD_FROM_EXPO

Make sure you move the keystore file that Expo generates into the android/app directory. Then simply set the value of  MYAPP_UPLOAD_STORE_FILE to be the name of your keystore file.

Now, let's add the signing config to our android/app/build.gradle file. Make sure you're editing the code in the proper sections of the file. Specifically the release section under signingConfigs and the release section under buildTypes.

...
android {
    ...
    defaultConfig { ... }
    signingConfigs {
        release {
            if (project.hasProperty('MYAPP_UPLOAD_STORE_FILE')) {
                storeFile file(MYAPP_UPLOAD_STORE_FILE)
                storePassword MYAPP_UPLOAD_STORE_PASSWORD
                keyAlias MYAPP_UPLOAD_KEY_ALIAS
                keyPassword MYAPP_UPLOAD_KEY_PASSWORD
            }
        }
    }
    buildTypes {
        release {
            ...
            signingConfig signingConfigs.release
        }
    }
}
...

Once we do all that, all that is left for us to do is to generate our release APK.

cd android
./gradlew bundleRelease

While building your APK, you might run into this error

Expiring Daemon because JVM heap space is exhausted

To fix it, open your gradle.properties file and add these two lines

org.gradle.daemon=true
org.gradle.jvmargs=-Xmx2560m

You can find the generated file under android/app/build/outputs/bundle/release.

You probably also want to test it before doing that. To test the app, simply run:

npx react-native run-android --variant=release

Make sure you uninstall any previous version of the app that you may already have on the device.

If your build works fine, that's great. Test it well and move onto the next section. For me, it didn't. The app would crash as soon as I opened it and I had to do a bunch of things before I got it to work again. If you're facing the same issue, you might want to continue reading and try some of these techniques.

The best way to figure out why your app is crashing would be to look at the logs, to do that, run adb logcat *:E, wait for it to output whatever it outputs and once it stops/slows down, try opening your app and you should be able to see the stack trace.

Expo Publish

Another issue I faced was with the fact that I had never run expo publish before. This command, along with publishing your app, also creates the manifest and the bundle in your respective iOS and Android directories. Those files are necessary once you build your app and run it in production. Simply run expo publish and it'll create the necessary files in the right place and that should that care of that.

A small note here, you must run expo publish every time you want to release a new version of your app. The manifest and bundle files it creates are basically the JS bundle that contains the code for your app. Your ideal process should be something like this expo publish -> bundle -> test -> release.

Another small note. If you have OTA updates on (and they're on by default), this may break the app for the users that are already using it. I am not quite sure how to work around this but I personally have turned them off so this doesn't happen in the future and will look into turning them back on later.

Assets

After running expo publish, I ran into a new problem. My app would throw an error saying certain assets were missing. The reason behind this is the bundledAssets key in the manifest that Expo generates. To fix this, I had to tell Expo to generate and bundle those assets in the standalone binary. To do that, simply edit/add the assetBundlePatterns key to your app.json with the path to all your assets. Here's what mine looks like:

{
  "expo": {
    "assetBundlePatterns": ["assets/fonts/*", "assets/svgs/*", "assets/*"],
  }
}

Once I fixed these issues, I rebuilt my app and it finally launched and proceeded to work perfectly on my device!

Proguard

So apparently this helps reduce your app size so let's give this a shot as well. To enable Proguard, open your android/app/build.gradle and set this to true.

def enableProguardInReleaseBuilds = true

Once, I did this and built an APK, it was 2MB smaller but it crashed as soon as I opened it. The reason for this was that I was using the react-native-svg package in my app. To fix the crash, I had to add the following snippet to my proguard-rules.pro file.

-keep public class com.horcrux.svg.** {*;}

After doing that and building my app again, everything seemed to work as expected.

Release

At this point, we have a release build that works and the only thing left to do is upload our app to the Play Store so our users can get access to it. Well... almost!

Permissions

This was something I noticed after I built my app and tried to release it. Somehow, my app now needed every single permission ever to run. I don't quite know what caused this to happen but you may want to check your AndroidManifest.xml file and comment out the permissions you don't need.

Another issue I faced with permissions was that even though I commented some of them out, my app would still ask for them. This probably happens because one of the packages (probably a unimodule) in your project might be asking for them even though you don't need them. To fix this, you explicitly need to add tools:node="remove" to that permission. Here's a small snippet.

<manifest
  xmlns:android="http://schemas.android.com/apk/res/android"
  <!-- You need this line to use the tools namespace -->
  xmlns:tools="http://schemas.android.com/tools"
  package="YOUR_PACKAGE_NAME"
>
  <uses-permission tools:node="remove" android:name="android.permission.ACCESS_COARSE_LOCATION"/>
</manifest>
AndroidManifest.xml

Versioning

Once we've tested our build and made sure everything works as expected, we want to update our versionCode and versionName. Earlier, we'd do this in the app.json but because we've ejected to the bare workflow, we must now do it in the android/app/build.gradle file. Remember, the versionCode must be an Integer while the versionName is a String.

After updating the values, build your app one last time and now, you can upload it to the Play Store and upon review, it should reach your users!


It's been a long day so I am going to stop here for now. I may in the future explore reducing the app size further and seeing how that goes but in the meantime I came back to update this! Upon pushing the release to the Play Store, I noticed that the app was now only 9.62MB compared to the 25MB it was earlier! I hope this post helps you and if you have any questions, feel free to hit me up on Twitter with them!

Tags:

Faraz Patankar

Probably obsessing over fantasy football, figuring out what to eat for my next meal or working on my next great (soon to be abandoned) side project but hello to you too! 🤗

More posts from this author

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.