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 { Terminal } from '~/ui/components/Snippet'; 8import { BoxLink } from '~/ui/components/BoxLink'; 9import { BookOpen02Icon, Grid01Icon } from '@expo/styleguide-icons'; 10 11In 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. 12 13## 1. Initialize a new module 14 15First, 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: 16 17<Terminal cmd={['$ npx create-expo-module expo-settings']} /> 18 19> **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. 20 21## 2. Set up our workspace 22 23Now 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. 24 25<Terminal 26 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" 27 cmd={[ 28 '$ cd expo-settings', 29 '$ rm ios/ExpoSettingsView.swift', 30 '$ rm android/src/main/java/expo/modules/settings/ExpoSettingsView.kt', 31 '$ rm src/ExpoSettingsView.tsx src/ExpoSettings.types.ts', 32 '$ rm src/ExpoSettingsView.web.tsx src/ExpoSettingsModule.web.ts', 33 ]} 34/> 35 36Find the following files and replace them with the provided minimal boilerplate: 37 38```swift ios/ExpoSettingsModule.swift 39import ExpoModulesCore 40 41public class ExpoSettingsModule: Module { 42 public func definition() -> ModuleDefinition { 43 Name("ExpoSettings") 44 45 Function("getTheme") { () -> String in 46 "system" 47 } 48 } 49} 50``` 51 52```kotlin android/src/main/java/expo/modules/settings/ExpoSettingsModule.kt 53package expo.modules.settings 54 55import expo.modules.kotlin.modules.Module 56import expo.modules.kotlin.modules.ModuleDefinition 57 58class ExpoSettingsModule : Module() { 59 override fun definition() = ModuleDefinition { 60 Name("ExpoSettings") 61 62 Function("getTheme") { 63 return@Function "system" 64 } 65 } 66} 67``` 68 69```typescript src/index.ts 70import ExpoSettingsModule from './ExpoSettingsModule'; 71 72export function getTheme(): string { 73 return ExpoSettingsModule.getTheme(); 74} 75``` 76 77```typescript example/App.tsx 78import * as Settings from 'expo-settings'; 79import { Text, View } from 'react-native'; 80 81export default function App() { 82 return ( 83 <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> 84 <Text>Theme: {Settings.getTheme()}</Text> 85 </View> 86 ); 87} 88``` 89 90## 3. Run the example project 91 92Now 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. 93 94<Terminal 95 cmdCopy="npm run build" 96 cmd={[ 97 '# Run this in the root of the project to start the TypeScript compiler', 98 '$ npm run build', 99 ]} 100/> 101 102<Terminal 103 cmdCopy="cd example && npx expo run:ios" 104 cmd={[ 105 '$ cd example', 106 '# Run the example app on iOS', 107 '$ npx expo run:ios', 108 '# Run the example app on Android', 109 '$ npx expo run:android', 110 ]} 111/> 112 113We 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. 114 115## 4. Get, set, and persist the theme preference value 116 117### iOS native module 118 119To 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. 120 121To set the value, we can use `UserDefaults`'s `set(_:forKey:)` method. We'll make our `setTheme` function accept a value of type `String`. 122 123```swift ios/ExpoSettingsModule.swift 124import ExpoModulesCore 125 126public class ExpoSettingsModule: Module { 127 public func definition() -> ModuleDefinition { 128 Name("ExpoSettings") 129 130 Function("setTheme") { (theme: String) -> Void in 131 UserDefaults.standard.set(theme, forKey:"theme") 132 } 133 134 Function("getTheme") { () -> String in 135 UserDefaults.standard.string(forKey: "theme") ?? "system" 136 } 137 } 138} 139``` 140 141### Android native module 142 143To 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()`. 144 145To 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`. 146 147```kotlin android/src/main/java/expo/modules/settings/ExpoSettingsModule.kt 148package expo.modules.settings 149 150import android.content.Context 151import android.content.SharedPreferences 152import expo.modules.kotlin.modules.Module 153import expo.modules.kotlin.modules.ModuleDefinition 154 155class ExpoSettingsModule : Module() { 156 override fun definition() = ModuleDefinition { 157 Name("ExpoSettings") 158 159 Function("setTheme") { theme: String -> 160 getPreferences().edit().putString("theme", theme).commit() 161 } 162 163 Function("getTheme") { 164 return@Function getPreferences().getString("theme", "system") 165 } 166 } 167 168 private val context 169 get() = requireNotNull(appContext.reactContext) 170 171 private fun getPreferences(): SharedPreferences { 172 return context.getSharedPreferences(context.packageName + ".settings", Context.MODE_PRIVATE) 173 } 174} 175``` 176 177### TypeScript module 178 179Now we can call our native modules from TypeScript. 180 181```typescript src/index.ts 182import ExpoSettingsModule from './ExpoSettingsModule'; 183 184export function getTheme(): string { 185 return ExpoSettingsModule.getTheme(); 186} 187 188export function setTheme(theme: string): void { 189 return ExpoSettingsModule.setTheme(theme); 190} 191``` 192 193### Example app 194 195We can now use the `Settings` API in our example app. 196 197```typescript example/App.tsx 198import * as Settings from 'expo-settings'; 199import { Button, Text, View } from 'react-native'; 200 201export default function App() { 202 const theme = Settings.getTheme(); 203 // Toggle between dark and light theme 204 const nextTheme = theme === 'dark' ? 'light' : 'dark'; 205 206 return ( 207 <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> 208 <Text>Theme: {Settings.getTheme()}</Text> 209 <Button title={`Set theme to ${nextTheme}`} onPress={() => Settings.setTheme(nextTheme)} /> 210 </View> 211 ); 212} 213``` 214 215When 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. 216 217## 5. Emit change events for the theme value 218 219We 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 }`. 220 221### iOS native module 222 223```swift ios/ExpoSettingsModule.swift 224import ExpoModulesCore 225 226public class ExpoSettingsModule: Module { 227 public func definition() -> ModuleDefinition { 228 Name("ExpoSettings") 229 230 Events("onChangeTheme") 231 232 Function("setTheme") { (theme: String) -> Void in 233 UserDefaults.standard.set(theme, forKey:"theme") 234 sendEvent("onChangeTheme", [ 235 "theme": theme 236 ]) 237 } 238 239 Function("getTheme") { () -> String in 240 UserDefaults.standard.string(forKey: "theme") ?? "system" 241 } 242 } 243} 244``` 245 246### Android native module 247 248Events 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. 249 250```kotlin android/src/main/java/expo/modules/settings/ExpoSettingsModule.kt 251package expo.modules.settings 252 253import android.content.Context 254import android.content.SharedPreferences 255import androidx.core.os.bundleOf 256import expo.modules.kotlin.modules.Module 257import expo.modules.kotlin.modules.ModuleDefinition 258 259class ExpoSettingsModule : Module() { 260 override fun definition() = ModuleDefinition { 261 Name("ExpoSettings") 262 263 Events("onChangeTheme") 264 265 Function("setTheme") { theme: String -> 266 getPreferences().edit().putString("theme", theme).commit() 267 [email protected]("onChangeTheme", bundleOf("theme" to theme)) 268 } 269 270 Function("getTheme") { 271 return@Function getPreferences().getString("theme", "system") 272 } 273 } 274 275 private val context 276 get() = requireNotNull(appContext.reactContext) 277 278 private fun getPreferences(): SharedPreferences { 279 return context.getSharedPreferences(context.packageName + ".settings", Context.MODE_PRIVATE) 280 } 281} 282``` 283 284### TypeScript module 285 286```typescript src/index.ts 287import { EventEmitter, Subscription } from 'expo-modules-core'; 288import ExpoSettingsModule from './ExpoSettingsModule'; 289 290const emitter = new EventEmitter(ExpoSettingsModule); 291 292export type ThemeChangeEvent = { 293 theme: string; 294}; 295 296export function addThemeListener(listener: (event: ThemeChangeEvent) => void): Subscription { 297 return emitter.addListener<ThemeChangeEvent>('onChangeTheme', listener); 298} 299 300export function getTheme(): string { 301 return ExpoSettingsModule.getTheme(); 302} 303 304export function setTheme(theme: string): void { 305 return ExpoSettingsModule.setTheme(theme); 306} 307``` 308 309### Example app 310 311```typescript example/App.tsx 312import * as Settings from 'expo-settings'; 313import * as React from 'react'; 314import { Button, Text, View } from 'react-native'; 315 316export default function App() { 317 const [theme, setTheme] = React.useState<string>(Settings.getTheme()); 318 319 React.useEffect(() => { 320 const subscription = Settings.addThemeListener(({ theme: newTheme }) => { 321 setTheme(newTheme); 322 }); 323 324 return () => subscription.remove(); 325 }, [setTheme]); 326 327 // Toggle between dark and light theme 328 const nextTheme = theme === 'dark' ? 'light' : 'dark'; 329 330 return ( 331 <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> 332 <Text>Theme: {Settings.getTheme()}</Text> 333 <Button title={`Set theme to ${nextTheme}`} onPress={() => Settings.setTheme(nextTheme)} /> 334 </View> 335 ); 336} 337``` 338 339## 6. Improve type safety with Enums 340 341It'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`. 342 343### iOS native module 344 345```swift ios/ExpoSettingsModule.swift 346import ExpoModulesCore 347 348public class ExpoSettingsModule: Module { 349 public func definition() -> ModuleDefinition { 350 Name("ExpoSettings") 351 352 Events("onChangeTheme") 353 354 Function("setTheme") { (theme: Theme) -> Void in 355 UserDefaults.standard.set(theme.rawValue, forKey:"theme") 356 sendEvent("onChangeTheme", [ 357 "theme": theme.rawValue 358 ]) 359 } 360 361 Function("getTheme") { () -> String in 362 UserDefaults.standard.string(forKey: "theme") ?? Theme.system.rawValue 363 } 364 } 365 366 enum Theme: String, Enumerable { 367 case light 368 case dark 369 case system 370 } 371} 372``` 373 374### Android native module 375 376```kotlin android/src/main/java/expo/modules/settings/ExpoSettingsModule.kt 377package expo.modules.settings 378 379import android.content.Context 380import android.content.SharedPreferences 381import androidx.core.os.bundleOf 382import expo.modules.kotlin.modules.Module 383import expo.modules.kotlin.modules.ModuleDefinition 384import expo.modules.kotlin.types.Enumerable 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 Android and iOS. 460 461You can con continue to the next tutorial to learn how to create a native view or see the Expo module API reference. 462 463<BoxLink 464 title="Creating a native view" 465 description="A tutorial on creating a native view that renders a WebView with Expo modules API." 466 href="/modules/native-view-tutorial/" 467 Icon={BookOpen02Icon} 468/> 469 470<BoxLink 471 title="Module API Reference" 472 description="Outline for the Expo Module API and common patterns like sending events from native code to JavaScript." 473 href="/modules/module-api/" 474 Icon={Grid01Icon} 475/> 476