教程:创建一个原生模块
编辑页面
使用 Expo Modules API 创建一个可持久化设置的原生模块教程。
For the complete documentation index, see llms.txt. Use this Use this file to discover all available pages.
在本教程中,你将构建一个模块,用于存储用户偏好的应用主题:深色、浅色或系统默认。在 Android 上,使用 SharedPreferences;在 iOS 上,使用 UserDefaults。你还可以使用 localStorage 实现 Web 支持,但本教程不涉及这部分。

构建一个使用 SharedPreferences(Android)和 UserDefaults(iOS)持久化用户设置的原生模块。
1
2
设置工作区
清理默认模块,从一个干净的状态开始。删除视图模块,因为本指南不会用到它。
- 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找到以下文件,并将其内容替换为提供的最小样板代码:
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; } // 此调用会从 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>主题:{Settings.getTheme()}</Text> </View> ); }
3
4
获取、设置并持久化主题偏好值
Android 原生模块
要读取该值,请查找键 "theme" 下的 SharedPreferences 字符串。如果该键不存在,则默认值为 "system"。使用 reactContext(React Native 的 ContextWrapper)通过 getSharedPreferences() 访问 SharedPreferences 实例。
要设置该值,请使用 SharedPreferences 的 edit() 方法获取一个 Editor 实例。然后使用 putString() 设置值。确保 setTheme 函数接受 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 上读取该值,请查找键 "theme" 下的 UserDefaults 字符串。如果该键不存在,则默认值为 "system"。
要设置该值,请使用 UserDefaults 的 set(_:forKey:) 方法。确保 setTheme 函数接受 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 模块
更新 ExpoSettingsModule.ts,为 ExpoSettingsModule 原生模块添加一个 TypeScript 接口,以便更新主题。
import { NativeModule, requireNativeModule } from 'expo'; import { ExpoSettingsModuleEvents } from './ExpoSettings.types'; declare class ExpoSettingsModule extends NativeModule<ExpoSettingsModuleEvents> { setTheme: (theme: string) => void; getTheme: () => string; } // 此调用会从 JSI 加载原生模块对象。 export default requireNativeModule<ExpoSettingsModule>('ExpoSettings');
现在,从 TypeScript 中调用你的原生模块。
import ExpoSettingsModule from './ExpoSettingsModule'; export function getTheme(): string { return ExpoSettingsModule.getTheme(); } export function setTheme(theme: string): void { return ExpoSettingsModule.setTheme(theme); }
示例应用
现在你可以在示例应用中使用 Settings API。
import * as Settings from 'expo-settings'; import { Button, Text, View } from 'react-native'; export default function App() { const theme = Settings.getTheme(); // 在深色和浅色主题之间切换 const nextTheme = theme === 'dark' ? 'light' : 'dark'; return ( <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> <Text>主题:{Settings.getTheme()}</Text> <Button title={`将主题设为 ${nextTheme}`} onPress={() => Settings.setTheme(nextTheme)} /> </View> ); }
当你重新构建并运行应用时,仍然会设置为“system”主题。点击按钮不会产生任何效果,但当你重新加载应用时,主题会发生变化。这是因为应用没有获取新的主题值或重新渲染。你将在下一步中修复这个问题。
5
为主题值发送变更事件
确保使用你的 API 的开发者可以在主题值变化时做出响应:每当值更新时发送一个变更事件。使用 Events 定义组件来描述模块发出的事件,使用 sendEvent 从原生代码发送事件,并使用 EventEmitter API 在 JavaScript 中订阅事件。事件负载为 { theme: string }。
Android 原生模块
事件负载在 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 原生模块
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 模块
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); }
示例应用
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]); // 在深色和浅色主题之间切换 const nextTheme = theme === 'dark' ? 'light' : 'dark'; return ( <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> <Text>主题:{Settings.getTheme()}</Text> <Button title={`将主题设为 ${nextTheme}`} onPress={() => Settings.setTheme(nextTheme)} /> </View> ); }
6
使用枚举提高类型安全
在当前形式下使用 Settings.setTheme() API 很容易出错,因为它允许任何字符串值。通过使用枚举将可能的值限制为 system、light 和 dark,来提升这个 API 的类型安全性。
Android 原生模块
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 原生模块
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 模块
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; } // 此调用会从 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); }
示例应用
如果你将 Settings.setTheme(nextTheme) 改为 Settings.setTheme("not-a-real-theme"),TypeScript 会报错。如果你忽略该错误并点击按钮,你会看到如下运行时错误:
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。
恭喜!你已经为 Android 和 iOS 创建了你的第一个 Expo 模块。
下一步
使用 Kotlin 和 Swift 创建原生模块。
关于使用 Expo Modules API 创建原生视图的教程。