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