教程:创建原生模块
编辑
使用 Expo 模块 API 创建持久化设置的原生模块教程。
在本教程中,您将构建一个模块,该模块存储用户首选的应用主题:暗色、亮色或系统。Android 上使用 SharedPreferences,而在 iOS 上使用 UserDefaults。您还可以通过 localStorage 实现 Web 支持,但本教程不会涵盖这一部分。

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; } // 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
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; } // This call loads the native module object from the 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); }
示例应用
您现在可以在示例应用中使用设置 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>Theme: {Settings.getTheme()}</Text> <Button title={`Set theme to ${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>Theme: {Settings.getTheme()}</Text> <Button title={`Set theme to ${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; } // 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); }
示例应用
如果您将 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 模块 API 创建原生视图的教程。