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 (opens in a new tab) 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 (opens in a new tab) 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 (opens in a new tab) 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.
Luckily Expo allows us to configure the Android status bar through app.json (opens in a new tab) 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.
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 (opens in a new tab). 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 (opens in a new tab) - 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 (opens in a new tab)). 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 (opens in a new tab). 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.