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