A Step By Step Guide To Writing Your First Expo Config Plugin

Frustrated when your managed Expo project misbehaves and you have no option to customize it? Well, that is a thing of the past now. This tutorial will teach you how to start with Expo config plugins which allow you to add custom native Android and iOS configurations without ejecting the managed workflow. We will use a practical example by debugging and fixing Android's status bar translucent behavior to allow our splash screen reach all device edges.

INITIAL SETUP

To initiate our project you can simply clone this repository that has all demonstrative assets included. Or you can start yourself by running expo-cli:

expo init first-config-plugin -t expo-template-blank-typescript

Then add expo-splash-screen as your dependency..

expo install expo-splash-screen

..and necessary assets to reproduce the issue. We are using typescript for better development experience, which you will definitely find it helpful writing the plugins.

THE ISSUE

We want our application to look cool and therefore we show a full screen image on the intro page. For that we need to set the status bar to be translucent so that image can stretch under it.

<StatusBar style="dark" translucent backgroundColor="transparent" />

The issue is that the status bar is not translucent from the start, resulting in our splash screen and logo shifting up once the translucency takes effect on the app mount. Notice also the color shift of the status bar.

non translucent status bar demo

Click for better resolution

Luckily Expo allows us to configure the Android status bar through app.json which will set the default behavior before the app mounts. Lets try to replicate our settings:

"androidStatusBar": {
      "backgroundColor": "#00000000",
      "barStyle": "dark-content",
      "translucent": true
    }

The result is not exactly what we wanted. While we managed to make the color of status bar dark from the app launch and logo is not shifting up anymore as translucency is taking effect, the bar itself has that ugly overlay.

translucent status bar demo

Click for better resolution

We need to investigate what is going on. By running expo run:android or expo prebuild -p android we can generate the Android folder with its configurations. If you don't know where the issue is coming from, you can use a simple strategy to see where changes are being made. Just stage all generated Android files (git add -A), remove translucent: true from app.json, and run expo prebuild -p android to generate native files again.

By removing the translucent property you should see following unstaged changes:

android/app/src/main/res/values/strings.xml

- <string name="expo_splash_screen_status_bar_translucent" translatable="false">true</string>
+ <string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>

android/app/src/main/res/values/styles.xml

- <item name="android:windowTranslucentStatus">true</item>

This already looks promising as the expo_splash_screen_status_bar_translucent custom Expo property sounds exactly like something we are trying to influence and being previously true we saw the status bar was really translucent during the splash screen. We also notice that android:windowTranslucentStatus was set to true which with some googling we realize was responsible for the status bar overlay. If you are in managed workflow, in the past you would feel trapped because you can't have one without the other to reach the behavior you wanted and the only option was to raise it in Expo issues. But now thanks to Expo config plugins you can take the solution in your hands.

THE SOLUTION

The solution is to make expo_splash_screen_status_bar_translucent set to true while keeping android:windowTranslucentStatus as false - which is the default therefore lets change only the Expo property. With expo run:android you can actually change it in your Android native files yourself to prove the effect after a new build. Yet if we want to stay in managed workflow and away from native file changes, we need to write a custom plugin which will do the change for us during the prebuild.

This means any native changes through plugins will be reflected in Expo development environment only using expo-dev-client - our custom Expo Go.

The @expo/config-plugins package is already part of Expo, so we don't need to install any new dependency. We will start by creating our plugin file in typescript, which is a recommended approach and can be useful not only for more advanced changes.

Lets create our initial plugin file withAndroidSplashScreen.ts in the root folder:

import type { ConfigPlugin } from '@expo/config-plugins'
import { withStringsXml } from '@expo/config-plugins'

const withAndroidSplashScreen: ConfigPlugin = (expoConfig) =>
  withStringsXml(expoConfig, (modConfig) => {
    return modConfig
  })

export default withAndroidSplashScreen

And start compiling it into javascript:

yarn tsc withAndroidSplashScreen.ts --watch --skipLibCheck

Finally, import resulting withAndroidSplashScreen.js file into app.json plugins property for Expo to process it on a next build. Our changes look like this:

{
  "expo": {
    ...otherProps,
    "androidStatusBar": {
      "backgroundColor": "#00000000",
      "barStyle": "dark-content"
    },
    "plugins": ["./withAndroidSplashScreen.js"]
  }
}

Now you can run expo prebuild -p android to see effects of your plugin. Obviously, if you inspect our withAndroidSplashScreen code it is not changing anything yet. It just returns whatever it receives. Our plugin is a simple function.

Initially our plugin receives expoConfig which is basically content of app.json and this object is passed to the withStringXml mod. This particular mod (modifier) from Expo enables us to read contents of android/app/src/main/res/values/strings.xml and change them based on what config we return (all available mods can be found here). For each mod its content can be read from modConfig.modResults - you can actually use console.log(JSON.stringify(config.modResults, null, 2)); to inspect the values during the prebuild command. To apply our desired changes we need to modify modResults.

import type { ConfigPlugin } from '@expo/config-plugins'
import { AndroidConfig, withStringsXml } from '@expo/config-plugins'

const withAndroidSplashScreen: ConfigPlugin = (expoConfig) =>
  withStringsXml(expoConfig, (modConfig) => {
    modConfig.modResults = AndroidConfig.Strings.setStringItem(
      [
        {
          _: 'true',
          $: {
            name: 'expo_splash_screen_status_bar_translucent',
            translatable: 'false'
          }
        }
      ],
      modConfig.modResults
    )
    return modConfig
  })

export default withAndroidSplashScreen

As you see, we assign to modResults what is returned from AndroidConfig helper method setStringItem which accepts the value we want to add and then remaining file strings already existing. Inspecting type of setStringItem and typescript in general should help you fill all needed properties correctly. After running prebuild we should see a new configuration string:

+ <string name="expo_splash_screen_status_bar_translucent" translatable="false">true</string>

We now have our desired splash screen behavior with a translucent status bar already from the app start and without the ugly overlay.

SUMMARY

Hopefully this tutorial helped you to understand better the power of config plugins and that customizing your Expo project is actually not that difficult - you can see final solution in this branch. If you ask what to do with the native Android folder now when you are finished with debugging, you can just delete it together with all generated files. Important is to commit your new plugin file and changes in app.json. The prebuild command is a part of EAS build so next time you build your project, you can be sure your plugin will take effect the same way you did it locally.

18