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/).