1--- 2title: 'Tutorial: Creating a native module' 3sidebar_title: Create a native module 4description: A tutorial on creating a native module with Expo modules API. 5--- 6 7import { CodeBlocksTable } from '~/components/plugins/CodeBlocksTable'; 8import { PlatformTag } from '~/ui/components/Tag'; 9import { APIBox } from '~/components/plugins/APIBox'; 10import { Terminal } from '~/ui/components/Snippet'; 11 12In this tutorial, we are going to build a module that stores the user's preferred app theme - either dark, light, or system. We'll use [`UserDefaults`](https://developer.apple.com/documentation/foundation/userdefaults) on iOS and [`SharedPreferences`](https://developer.android.com/reference/android/content/SharedPreferences) on Android. It is possible to implement web support using [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage), but we'll leave that as an exercise for the reader. 13 14## 1. Initialize a new module 15 16First, we'll create a new module. On this page we will use the name `expo-settings`/`ExpoSettings`. You can name it whatever you like, just adjust the instructions accordingly: 17 18<Terminal cmd={['$ npx create-expo-module expo-settings']} /> 19 20> **Tip**: Since you aren't going to actually ship this library, you can hit <kbd>return</kbd> for all of the prompts to accept the default values. 21 22## 2. Set up our workspace 23 24Now let's clean up the default module a little bit so we have more of a clean slate and delete the view module that we won't use in this guide. 25 26<Terminal 27 cmdCopy="cd expo-settings && rm ios/ExpoSettingsView.swift && rm android/src/main/java/expo/modules/settings/ExpoSettingsView.kt && rm src/ExpoSettingsView.tsx src/ExpoSettingsView.web.tsx src/ExpoSettingsModule.web.ts src/ExpoSettings.types.ts" 28 cmd={[ 29 '$ cd expo-settings', 30 '$ rm ios/ExpoSettingsView.swift', 31 '$ rm android/src/main/java/expo/modules/settings/ExpoSettingsView.kt', 32 '$ rm src/ExpoSettingsView.tsx src/ExpoSettings.types.ts', 33 '$ rm src/ExpoSettingsView.web.tsx src/ExpoSettingsModule.web.ts', 34 ]} 35/> 36 37Find the following files and replace them with the provided minimal boilerplate: 38 39```swift ios/ExpoSettingsModule.swift 40import ExpoModulesCore 41 42public class ExpoSettingsModule: Module { 43 public func definition() -> ModuleDefinition { 44 Name("ExpoSettings") 45 46 Function("getTheme") { () -> String in 47 "system" 48 } 49 } 50} 51``` 52 53```kotlin android/src/main/java/expo/modules/settings/ExpoSettingsModule.kt 54package expo.modules.settings 55 56import expo.modules.kotlin.modules.Module 57import expo.modules.kotlin.modules.ModuleDefinition 58 59class ExpoSettingsModule : Module() { 60 override fun definition() = ModuleDefinition { 61 Name("ExpoSettings") 62 63 Function("getTheme") { 64 return@Function "system" 65 } 66 } 67} 68``` 69 70```typescript src/index.ts 71import ExpoSettingsModule from './ExpoSettingsModule'; 72 73export function getTheme(): string { 74 return ExpoSettingsModule.getTheme(); 75} 76``` 77 78```typescript example/App.tsx 79import * as Settings from 'expo-settings'; 80import { Text, View } from 'react-native'; 81 82export default function App() { 83 return ( 84 <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> 85 <Text>Theme: {Settings.getTheme()}</Text> 86 </View> 87 ); 88} 89``` 90 91## 3. Run the example project 92 93Now let's run the example project to make sure everything is working. We'll need to start the TypeScript compiler to watch for changes and rebuild the module JavaScript, and separately in another terminal window we'll compile and run the example app. 94 95<Terminal 96 cmdCopy="npm run build" 97 cmd={[ 98 '# Run this in the root of the project to start the TypeScript compiler', 99 '$ npm run build', 100 ]} 101/> 102 103<Terminal 104 cmdCopy="cd example && npx expo run:ios" 105 cmd={[ 106 '$ cd example', 107 '# Run the example app on iOS', 108 '$ npx expo run:ios', 109 '# Run the example app on Android', 110 '$ npx expo run:android', 111 ]} 112/> 113 114We should now see the text "Theme: system" in the center of the screen when we launch the example app. The value `"system"` is the result of synchronously calling the `getTheme()` function in the native module. We'll change this value in the next step. 115 116## 4. Get, set, and persist the theme preference value 117 118### iOS native module 119 120To read the value on iOS, we can look for a `UserDefaults` string under the key `"theme"`, and fall back to `"system"` if there isn't any. 121 122To set the value, we can use `UserDefaults`'s `set(_:forKey:)` method. We'll make our `setTheme` function accept a value of type `String`. 123 124```swift ios/ExpoSettingsModule.swift 125import ExpoModulesCore 126 127public class ExpoSettingsModule: Module { 128 public func definition() -> ModuleDefinition { 129 Name("ExpoSettings") 130 131 Function("setTheme") { (theme: String) -> Void in 132 UserDefaults.standard.set(theme, forKey:"theme") 133 } 134 135 Function("getTheme") { () -> String in 136 UserDefaults.standard.string(forKey: "theme") ?? "system" 137 } 138 } 139} 140``` 141 142### Android native module 143 144To read the value, we can look for a `SharedPreferences` string under the key `"theme"`, and fall back to `"system"` if there isn't any. We can get the `SharedPreferences` instance from the `reactContext` (a React Native [ContextWrapper](https://developer.android.com/reference/android/content/ContextWrapper)) using `getSharedPreferences()`. 145 146To set the value, we can use `SharedPreferences`'s `edit()` method to get an `Editor` instance, and then use `putString()` to set the value. We'll make our `setTheme` function accept a value of type `String`. 147 148```kotlin android/src/main/java/expo/modules/settings/ExpoSettingsModule.kt 149package expo.modules.settings 150 151import android.content.Context 152import android.content.SharedPreferences 153import expo.modules.kotlin.modules.Module 154import expo.modules.kotlin.modules.ModuleDefinition 155 156class ExpoSettingsModule : Module() { 157 override fun definition() = ModuleDefinition { 158 Name("ExpoSettings") 159 160 Function("setTheme") { theme: String -> 161 getPreferences().edit().putString("theme", theme).commit() 162 } 163 164 Function("getTheme") { 165 return@Function getPreferences().getString("theme", "system") 166 } 167 } 168 169 private val context 170 get() = requireNotNull(appContext.reactContext) 171 172 private fun getPreferences(): SharedPreferences { 173 return context.getSharedPreferences(context.packageName + ".settings", Context.MODE_PRIVATE) 174 } 175} 176``` 177 178### TypeScript module 179 180Now we can call our native modules from TypeScript. 181 182```typescript src/index.ts 183import ExpoSettingsModule from './ExpoSettingsModule'; 184 185export function getTheme(): string { 186 return ExpoSettingsModule.getTheme(); 187} 188 189export function setTheme(theme: string): void { 190 return ExpoSettingsModule.setTheme(theme); 191} 192``` 193 194### Example app 195 196We can now use the `Settings` API in our example app. 197 198```typescript example/App.tsx 199import * as Settings from 'expo-settings'; 200import { Button, Text, View } from 'react-native'; 201 202export default function App() { 203 const theme = Settings.getTheme(); 204 // Toggle between dark and light theme 205 const nextTheme = theme === 'dark' ? 'light' : 'dark'; 206 207 return ( 208 <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> 209 <Text>Theme: {Settings.getTheme()}</Text> 210 <Button title={`Set theme to ${nextTheme}`} onPress={() => Settings.setTheme(nextTheme)} /> 211 </View> 212 ); 213} 214``` 215 216When we re-build and run the app, we'll see the "system" them is still set. When we press the button, nothing happens. When you reload the app, you'll see the theme has changed. This is because we're never fetching the new theme value and re-rendering the app. We'll fix this in the next step. 217 218## 5. Emit change events for the theme value 219 220We can ensure that developers using our API can react to changes in the theme value by emitting a change event when the value changes. We'll use the [Events](/modules/module-api/#events) definition component to describe events that our module can emit, `sendEvent` to emit the event from native, and the [EventEmitter](/modules/module-api/#sending-events) API to subscribe to events in JavaScript. Our event payload will be `{ theme: string }`. 221 222### iOS native module 223 224```swift ios/ExpoSettingsModule.swift 225import ExpoModulesCore 226 227public class ExpoSettingsModule: Module { 228 public func definition() -> ModuleDefinition { 229 Name("ExpoSettings") 230 231 Events("onChangeTheme") 232 233 Function("setTheme") { (theme: String) -> Void in 234 UserDefaults.standard.set(theme, forKey:"theme") 235 sendEvent("onChangeTheme", [ 236 "theme": theme 237 ]) 238 } 239 240 Function("getTheme") { () -> String in 241 UserDefaults.standard.string(forKey: "theme") ?? "system" 242 } 243 } 244} 245``` 246 247### Android native module 248 249Events payloads are represented as [`Bundle`](https://developer.android.com/reference/android/os/Bundle.html) instances on Android, which we can create using the [`bundleOf`](<https://developer.android.com/reference/kotlin/androidx/core/os/package-summary#bundleOf(kotlin.Array)>) function. 250 251```kotlin android/src/main/java/expo/modules/settings/ExpoSettingsModule.kt 252package expo.modules.settings 253 254import android.content.Context 255import android.content.SharedPreferences 256import androidx.core.os.bundleOf 257import expo.modules.kotlin.modules.Module 258import expo.modules.kotlin.modules.ModuleDefinition 259 260class ExpoSettingsModule : Module() { 261 override fun definition() = ModuleDefinition { 262 Name("ExpoSettings") 263 264 Events("onChangeTheme") 265 266 Function("setTheme") { theme: String -> 267 getPreferences().edit().putString("theme", theme).commit() 268 [email protected]("onChangeTheme", bundleOf("theme" to theme)) 269 } 270 271 Function("getTheme") { 272 return@Function getPreferences().getString("theme", "system") 273 } 274 } 275 276 private val context 277 get() = requireNotNull(appContext.reactContext) 278 279 private fun getPreferences(): SharedPreferences { 280 return context.getSharedPreferences(context.packageName + ".settings", Context.MODE_PRIVATE) 281 } 282} 283``` 284 285### TypeScript module 286 287```typescript src/index.ts 288import { EventEmitter, Subscription } from 'expo-modules-core'; 289import ExpoSettingsModule from './ExpoSettingsModule'; 290 291const emitter = new EventEmitter(ExpoSettingsModule); 292 293export type ThemeChangeEvent = { 294 theme: string; 295}; 296 297export function addThemeListener(listener: (event: ThemeChangeEvent) => void): Subscription { 298 return emitter.addListener<ThemeChangeEvent>('onChangeTheme', listener); 299} 300 301export function getTheme(): string { 302 return ExpoSettingsModule.getTheme(); 303} 304 305export function setTheme(theme: string): void { 306 return ExpoSettingsModule.setTheme(theme); 307} 308``` 309 310### Example app 311 312```typescript example/App.tsx 313import * as Settings from 'expo-settings'; 314import * as React from 'react'; 315import { Button, Text, View } from 'react-native'; 316 317export default function App() { 318 const [theme, setTheme] = React.useState<string>(Settings.getTheme()); 319 320 React.useEffect(() => { 321 const subscription = Settings.addThemeListener(({ theme: newTheme }) => { 322 setTheme(newTheme); 323 }); 324 325 return () => subscription.remove(); 326 }, [setTheme]); 327 328 // Toggle between dark and light theme 329 const nextTheme = theme === 'dark' ? 'light' : 'dark'; 330 331 return ( 332 <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> 333 <Text>Theme: {Settings.getTheme()}</Text> 334 <Button title={`Set theme to ${nextTheme}`} onPress={() => Settings.setTheme(nextTheme)} /> 335 </View> 336 ); 337} 338``` 339 340## 6. Improve type safety with Enums 341 342It's easy for us to make a mistake when using the `Settings.setTheme()` API in its current form, because we can set the theme to any string value. We can improve the type safety of this API by using an enum to restrict the possible values to `system`, `light`, and `dark`. 343 344### iOS native module 345 346```swift ios/ExpoSettingsModule.swift 347import ExpoModulesCore 348 349public class ExpoSettingsModule: Module { 350 public func definition() -> ModuleDefinition { 351 Name("ExpoSettings") 352 353 Events("onChangeTheme") 354 355 Function("setTheme") { (theme: Theme) -> Void in 356 UserDefaults.standard.set(theme.rawValue, forKey:"theme") 357 sendEvent("onChangeTheme", [ 358 "theme": theme.rawValue 359 ]) 360 } 361 362 Function("getTheme") { () -> String in 363 UserDefaults.standard.string(forKey: "theme") ?? Theme.system.rawValue 364 } 365 } 366 367 enum Theme: String, Enumerable { 368 case light 369 case dark 370 case system 371 } 372} 373``` 374 375### Android native module 376 377```kotlin android/src/main/java/expo/modules/settings/ExpoSettingsModule.kt 378package expo.modules.settings 379 380import android.content.Context 381import android.content.SharedPreferences 382import androidx.core.os.bundleOf 383import expo.modules.kotlin.modules.Module 384import expo.modules.kotlin.modules.ModuleDefinition 385 386class ExpoSettingsModule : Module() { 387 override fun definition() = ModuleDefinition { 388 Name("ExpoSettings") 389 390 Events("onChangeTheme") 391 392 Function("setTheme") { theme: Theme -> 393 getPreferences().edit().putString("theme", theme.value).commit() 394 [email protected]("onChangeTheme", bundleOf("theme" to theme.value)) 395 } 396 397 Function("getTheme") { 398 return@Function getPreferences().getString("theme", Theme.SYSTEM.value) 399 } 400 } 401 402 private val context 403 get() = requireNotNull(appContext.reactContext) 404 405 private fun getPreferences(): SharedPreferences { 406 return context.getSharedPreferences(context.packageName + ".settings", Context.MODE_PRIVATE) 407 } 408} 409 410enum class Theme(val value: String) : Enumerable { 411 LIGHT("light"), 412 DARK("dark"), 413 SYSTEM("system") 414} 415``` 416 417### TypeScript module 418 419```typescript src/index.ts 420import { EventEmitter, Subscription } from 'expo-modules-core'; 421 422import ExpoSettingsModule from './ExpoSettingsModule'; 423 424const emitter = new EventEmitter(ExpoSettingsModule); 425 426export type Theme = 'light' | 'dark' | 'system'; 427 428export type ThemeChangeEvent = { 429 theme: Theme; 430}; 431 432export function addThemeListener(listener: (event: ThemeChangeEvent) => void): Subscription { 433 return emitter.addListener<ThemeChangeEvent>('onChangeTheme', listener); 434} 435 436export function getTheme(): Theme { 437 return ExpoSettingsModule.getTheme(); 438} 439 440export function setTheme(theme: Theme): void { 441 return ExpoSettingsModule.setTheme(theme); 442} 443``` 444 445### Example app 446 447If we change `Settings.setTheme(nextTheme)` to `Settings.setTheme("not-a-real-theme")`, TypeScript will complain, and if we ignore that and go ahead and press the button anyways, we'll see the following error: 448 449``` 450 ERROR Error: FunctionCallException: Calling the 'setTheme' function has failed (at ExpoModulesCore/SyncFunctionComponent.swift:76) 451→ Caused by: ArgumentCastException: Argument at index '0' couldn't be cast to type Enum<Theme> (at ExpoModulesCore/JavaScriptUtils.swift:41) 452→ Caused by: EnumNoSuchValueException: 'not-a-real-theme' is not present in Theme enum, it must be one of: 'light', 'dark', 'system' (at ExpoModulesCore/Enumerable.swift:37) 453``` 454 455We can see from the last line of the error message that `not-a-real-theme` is not a valid value for the `Theme` enum, and that `light`, `dark`, and `system` are the only valid values. 456 457## Next steps 458 459Congratulations, you have created your first simple yet non-trivial Expo module for iOS and Android! Learn more about the API in the [Expo Module API reference](/modules/module-api). 460 461If you enjoyed this tutorial and haven't done the native view tutorial, go to the ["Creating a native view" tutorial](/modules/native-view-tutorial) next. 462