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