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