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