教程:创建本地模块
关于使用 Expo Modules API 创建保留设置的原生模块的教程。
在本教程中,你将构建一个模块来存储用户偏好的应用主题:暗色、亮色或系统默认。在 Android 上,使用 SharedPreferences;在 iOS 上,使用 UserDefaults。你可以使用 localStorage 来实现网页支持,但本教程不涉及这一部分。
🌐 In this tutorial, you build a module that stores the user's preferred app theme: dark, light, or system. On Android, use SharedPreferences, and on iOS, use UserDefaults. You can implement web support with localStorage, but this tutorial does not cover that.

1
初始化一个新模块
🌐 Initialize a new module
首先,创建一个新模块。在本教程中,模块命名为 expo-settings 或 ExpoSettings。你可以选择其他名称,但需要相应调整说明以匹配你的选择。
🌐 First, create a new module. For this tutorial, the module is named expo-settings or ExpoSettings. You can choose a different name, but adjust the instructions to match your choice.
- npx create-expo-module expo-settings信息 由于你实际上不会发布这个库,你可以在所有提示中按 回车 以接受默认值。
2
设置工作区
🌐 Set up workspace
清理默认模块以从零开始。删除视图模块,因为本指南不使用它。
🌐 Clean up the default module to start with a clean slate. Delete the view module since this guide does not use it.
- cd expo-settings- rm ios/ExpoSettingsView.swift- rm android/src/main/java/expo/modules/settings/ExpoSettingsView.kt- rm src/ExpoSettingsView.tsx- rm src/ExpoSettingsView.web.tsx src/ExpoSettingsModule.web.ts找到以下文件,并将其内容替换为提供的最小化模板:
🌐 Locate the following files and replace their contents with the provided minimal boilerplate:
package expo.modules.settings import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition class ExpoSettingsModule : Module() { override fun definition() = ModuleDefinition { Name("ExpoSettings") Function("getTheme") { return@Function "system" } } }
import ExpoModulesCore public class ExpoSettingsModule: Module { public func definition() -> ModuleDefinition { Name("ExpoSettings") Function("getTheme") { () -> String in "system" } } }
export type ExpoSettingsModuleEvents = {};
import { NativeModule, requireNativeModule } from 'expo'; import { ExpoSettingsModuleEvents } from './ExpoSettings.types'; declare class ExpoSettingsModule extends NativeModule<ExpoSettingsModuleEvents> { getTheme: () => string; } // This call loads the native module object from the JSI. export default requireNativeModule<ExpoSettingsModule>('ExpoSettings');
import ExpoSettingsModule from './ExpoSettingsModule'; export function getTheme(): string { return ExpoSettingsModule.getTheme(); }
import * as Settings from 'expo-settings'; import { Text, View } from 'react-native'; export default function App() { return ( <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> <Text>Theme: {Settings.getTheme()}</Text> </View> ); }
3
运行示例项目
🌐 Run the example project
启动 TypeScript 编译器以监听更改。
🌐 Start the TypeScript compiler to watch for changes.
# Run this in the root of the project to start the TypeScript compiler- npm run build在另一个终端窗口中,运行示例应用。
🌐 In a separate terminal window, run the example app.
- cd example# Run the example app on Android- npx expo run:android# Run the example app on iOS- npx expo run:ios当你启动示例应用时,你应该在屏幕中央看到文本“主题:系统”。值 "system" 来自在本地模块中同步调用 getTheme() 函数。你将在下一步中更改此值。
🌐 You should see the text "Theme: system" in the center of the screen when you launch the example app. The value "system" comes from synchronously calling the getTheme() function in the native module. You will change this value in the next step.
4
获取、设置并保存主题偏好值
🌐 Get, set, and persist the theme preference value
安卓原生模块
🌐 Android native module
要读取该值,请在键 "theme" 下查找 SharedPreferences 字符串。如果该键不存在,则使用默认值 "system"。使用 reactContext(一个 React Native ContextWrapper)通过 getSharedPreferences() 访问 SharedPreferences 实例。
🌐 To read the value, look for a SharedPreferences string under the key "theme". If the key does not exist, default to "system". Use the reactContext (a React Native ContextWrapper) to access the SharedPreferences instance with getSharedPreferences().
要设置该值,请使用 SharedPreferences 的 edit() 方法获取一个 Editor 实例。然后,使用 putString() 设置该值。确保 setTheme 函数接受类型为 String 的值。
🌐 To set the value, use the edit() method of SharedPreferences to get an Editor instance. Then, use putString() to set the value. Ensure the setTheme function accepts a value of type String.
package expo.modules.settings import android.content.Context import android.content.SharedPreferences import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition class ExpoSettingsModule : Module() { override fun definition() = ModuleDefinition { Name("ExpoSettings") Function("setTheme") { theme: String -> getPreferences().edit().putString("theme", theme).commit() } Function("getTheme") { return@Function getPreferences().getString("theme", "system") } } private val context get() = requireNotNull(appContext.reactContext) private fun getPreferences(): SharedPreferences { return context.getSharedPreferences(context.packageName + ".settings", Context.MODE_PRIVATE) } }
iOS 原生模块
🌐 iOS native module
要在 iOS 上读取该值,请查找键 "theme" 下的 UserDefaults 字符串。如果该键不存在,则默认为 "system"。
🌐 To read the value on iOS, look for a UserDefaults string under the key "theme". If the key does not exist, default to "system".
要设置该值,请使用 UserDefaults 的 set(_:forKey:) 方法。确保 setTheme 函数接受类型为 String 的值。
🌐 To set the value, use the set(_:forKey:) method of UserDefaults. Ensure the setTheme function accepts a value of type String.
import ExpoModulesCore public class ExpoSettingsModule: Module { public func definition() -> ModuleDefinition { Name("ExpoSettings") Function("setTheme") { (theme: String) -> Void in UserDefaults.standard.set(theme, forKey:"theme") } Function("getTheme") { () -> String in UserDefaults.standard.string(forKey: "theme") ?? "system" } } }
TypeScript 模块
🌐 TypeScript module
更新 ExpoSettingsModule.ts,为 ExpoSettingsModule 原生模块添加一个 TypeScript 接口,以更新主题。
🌐 Update the ExpoSettingsModule.ts to add a TypeScript interface for the ExpoSettingsModule native module to update the theme.
import { NativeModule, requireNativeModule } from 'expo'; import { ExpoSettingsModuleEvents } from './ExpoSettings.types'; declare class ExpoSettingsModule extends NativeModule<ExpoSettingsModuleEvents> { setTheme: (theme: string) => void; getTheme: () => string; } // This call loads the native module object from the JSI. export default requireNativeModule<ExpoSettingsModule>('ExpoSettings');
现在,从 TypeScript 调用你的本地模块。
🌐 Now, call your native modules from TypeScript.
import ExpoSettingsModule from './ExpoSettingsModule'; export function getTheme(): string { return ExpoSettingsModule.getTheme(); } export function setTheme(theme: string): void { return ExpoSettingsModule.setTheme(theme); }
示例应用
🌐 Example app
你现在可以在你的示例应用中使用设置 API。
🌐 You can now use the Settings API in your example app.
import * as Settings from 'expo-settings'; import { Button, Text, View } from 'react-native'; export default function App() { const theme = Settings.getTheme(); // Toggle between dark and light theme const nextTheme = theme === 'dark' ? 'light' : 'dark'; return ( <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> <Text>Theme: {Settings.getTheme()}</Text> <Button title={`Set theme to ${nextTheme}`} onPress={() => Settings.setTheme(nextTheme)} /> </View> ); }
当你重新构建并运行应用时,仍然设置了“系统”主题。按下按钮没有任何反应,但当你重新加载应用时,主题会发生变化。这是因为应用没有获取新的主题值或重新渲染。你将在下一步解决这个问题。
🌐 When you rebuild and run the app, the "system" theme is still set. Pressing the button does nothing, but when you reload the app, the theme changes. This happens because the app does not fetch the new theme value or re-render. You will fix this in the next step.
5
为主题值触发更改事件
🌐 Emit change events for the theme value
确保使用你的 API 的开发者能够对主题值的更改做出反应,当值更新时发出一个更改事件。使用 Events 定义组件来描述你的模块发出的事件,使用 sendEvent 从原生代码触发事件,并使用 EventEmitter API 在 JavaScript 中订阅事件。事件负载是 { theme: string }。
🌐 Ensure developers using your API can react to theme value changes by emitting a change event whenever the value updates. Use the Events definition component to describe the events your module emits, sendEvent to emit the event from native code, and the EventEmitter API to subscribe to events in JavaScript. The event payload is { theme: string }.
安卓原生模块
🌐 Android native module
在 Android 上,事件负载表示为 Bundle 实例,你可以使用 bundleOf 函数来创建它们。
package expo.modules.settings import android.content.Context import android.content.SharedPreferences import androidx.core.os.bundleOf import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition class ExpoSettingsModule : Module() { override fun definition() = ModuleDefinition { Name("ExpoSettings") Events("onChangeTheme") Function("setTheme") { theme: String -> getPreferences().edit().putString("theme", theme).commit() this@ExpoSettingsModule.sendEvent("onChangeTheme", bundleOf("theme" to theme)) } Function("getTheme") { return@Function getPreferences().getString("theme", "system") } } private val context get() = requireNotNull(appContext.reactContext) private fun getPreferences(): SharedPreferences { return context.getSharedPreferences(context.packageName + ".settings", Context.MODE_PRIVATE) } }
iOS 原生模块
🌐 iOS native module
import ExpoModulesCore public class ExpoSettingsModule: Module { public func definition() -> ModuleDefinition { Name("ExpoSettings") Events("onChangeTheme") Function("setTheme") { (theme: String) -> Void in UserDefaults.standard.set(theme, forKey:"theme") sendEvent("onChangeTheme", [ "theme": theme ]) } Function("getTheme") { () -> String in UserDefaults.standard.string(forKey: "theme") ?? "system" } } }
TypeScript 模块
🌐 TypeScript module
export type ThemeChangeEvent = { theme: string; }; export type ExpoSettingsModuleEvents = { onChangeTheme: (params: ThemeChangeEvent) => void; };
import { EventSubscription } from 'expo-modules-core'; import ExpoSettingsModule from './ExpoSettingsModule'; import { ThemeChangeEvent } from './ExpoSettings.types'; export function addThemeListener(listener: (event: ThemeChangeEvent) => void): EventSubscription { return ExpoSettingsModule.addListener('onChangeTheme', listener); } export function getTheme(): string { return ExpoSettingsModule.getTheme(); } export function setTheme(theme: string): void { return ExpoSettingsModule.setTheme(theme); }
示例应用
🌐 Example app
import * as Settings from 'expo-settings'; import { useEffect, useState } from 'react'; import { Button, Text, View } from 'react-native'; export default function App() { const [theme, setTheme] = useState<string>(Settings.getTheme()); useEffect(() => { const subscription = Settings.addThemeListener(({ theme: newTheme }) => { setTheme(newTheme); }); return () => subscription.remove(); }, [setTheme]); // Toggle between dark and light theme const nextTheme = theme === 'dark' ? 'light' : 'dark'; return ( <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> <Text>Theme: {Settings.getTheme()}</Text> <Button title={`Set theme to ${nextTheme}`} onPress={() => Settings.setTheme(nextTheme)} /> </View> ); }
6
使用枚举提高类型安全
🌐 Improve type safety with Enums
在当前形式下使用 Settings.setTheme() API 容易出错,因为它允许任何字符串值。通过使用枚举来限制可能的值为 system、light 和 dark,可以提高该 API 的类型安全性。
🌐 It's easy to make mistakes when using the Settings.setTheme() API in its current form, as it allows any string value. Improve the type safety of this API by using an enum to restrict the possible values to system, light, and dark.
安卓原生模块
🌐 Android native module
package expo.modules.settings import android.content.Context import android.content.SharedPreferences import androidx.core.os.bundleOf import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition import expo.modules.kotlin.types.Enumerable class ExpoSettingsModule : Module() { override fun definition() = ModuleDefinition { Name("ExpoSettings") Events("onChangeTheme") Function("setTheme") { theme: Theme -> getPreferences().edit().putString("theme", theme.value).commit() this@ExpoSettingsModule.sendEvent("onChangeTheme", bundleOf("theme" to theme.value)) } Function("getTheme") { return@Function getPreferences().getString("theme", Theme.SYSTEM.value) } } private val context get() = requireNotNull(appContext.reactContext) private fun getPreferences(): SharedPreferences { return context.getSharedPreferences(context.packageName + ".settings", Context.MODE_PRIVATE) } } enum class Theme(val value: String) : Enumerable { LIGHT("light"), DARK("dark"), SYSTEM("system") }
iOS 原生模块
🌐 iOS native module
import ExpoModulesCore public class ExpoSettingsModule: Module { public func definition() -> ModuleDefinition { Name("ExpoSettings") Events("onChangeTheme") Function("setTheme") { (theme: Theme) -> Void in UserDefaults.standard.set(theme.rawValue, forKey:"theme") sendEvent("onChangeTheme", [ "theme": theme.rawValue ]) } Function("getTheme") { () -> String in UserDefaults.standard.string(forKey: "theme") ?? Theme.system.rawValue } } enum Theme: String, Enumerable { case light case dark case system } }
TypeScript 模块
🌐 TypeScript module
export type Theme = 'light' | 'dark' | 'system'; export type ThemeChangeEvent = { theme: Theme; }; export type ExpoSettingsModuleEvents = { onChangeTheme: (params: ThemeChangeEvent) => void; };
import { NativeModule, requireNativeModule } from 'expo'; import { ExpoSettingsModuleEvents, Theme } from './ExpoSettings.types'; declare class ExpoSettingsModule extends NativeModule<ExpoSettingsModuleEvents> { setTheme: (theme: Theme) => void; getTheme: () => Theme; } // This call loads the native module object from the JSI. export default requireNativeModule<ExpoSettingsModule>('ExpoSettings');
import { EventSubscription } from 'expo-modules-core'; import ExpoSettingsModule from './ExpoSettingsModule'; import { Theme, ThemeChangeEvent } from './ExpoSettings.types'; export function addThemeListener(listener: (event: ThemeChangeEvent) => void): EventSubscription { return ExpoSettingsModule.addListener('onChangeTheme', listener); } export function getTheme(): Theme { return ExpoSettingsModule.getTheme(); } export function setTheme(theme: Theme): void { return ExpoSettingsModule.setTheme(theme); }
示例应用
🌐 Example app
如果你将 Settings.setTheme(nextTheme) 改为 Settings.setTheme("not-a-real-theme"),TypeScript 会报错。如果你忽略错误并按下按钮,你将看到以下运行时错误:
🌐 If you change Settings.setTheme(nextTheme) to Settings.setTheme("not-a-real-theme"), TypeScript will raise an error. If you ignore the error and press the button, you will see the following runtime error:
ERROR Error: FunctionCallException: Calling the 'setTheme' function has failed (at ExpoModulesCore/SyncFunctionComponent.swift:76) → Caused by: ArgumentCastException: Argument at index '0' couldn't be cast to type Enum<Theme> (at ExpoModulesCore/JavaScriptUtils.swift:41) → 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)
错误消息的最后一行显示 not-a-real-theme 不是 Theme 枚举的有效值。唯一有效的值是 light、dark 和 system。
🌐 The last line of the error message shows that not-a-real-theme is not a valid value for the Theme enum. The only valid values are light, dark, and system.
恭喜!你已为 Android 和 iOS 创建了第一个 Expo 模块。
🌐 Congratulations! You have created your first Expo Module for Android and iOS.
下一步
🌐 Next steps
使用 Kotlin 和 Swift 创建原生模块。
使用 Expo 模块 API 创建原生视图的教程。