教程:创建原生模块

编辑

使用 Expo 模块 API 创建持久化设置的原生模块教程。


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

观看:如何使用 Expo 模块 API 创建原生模块
观看:如何使用 Expo 模块 API 创建原生模块

1

初始化新模块

首先,创建一个新模块。对于本教程,该模块名为 expo-settingsExpoSettings。您可以选择不同的名称,但请根据您的选择调整说明。

Terminal
npx create-expo-module expo-settings
由于您不打算实际发布这个库,因此您可以在所有提示中按 return 键以接受默认值。

2

设置工作区

清理默认模块,以从干净的状态开始。删除视图模块,因为本指南不使用它。

Terminal
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

找到以下文件并将其内容替换为提供的最小样板:

android/src/main/java/expo/modules/settings/ExpoSettingsModule.kt
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" } } }
ios/ExpoSettingsModule.swift
import ExpoModulesCore public class ExpoSettingsModule: Module { public func definition() -> ModuleDefinition { Name("ExpoSettings") Function("getTheme") { () -> String in "system" } } }
src/ExpoSettings.types.ts
export type ExpoSettingsModuleEvents = {};
src/ExpoSettingsModule.ts
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');
src/index.ts
import ExpoSettingsModule from './ExpoSettingsModule'; export function getTheme(): string { return ExpoSettingsModule.getTheme(); }
example/App.tsx
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

运行示例项目

启动 TypeScript 编译器以监视更改。

Terminal
# 在项目根目录运行以启动 TypeScript 编译器
npm run build

在一个单独的终端窗口中,运行示例应用程序。

Terminal
cd example
# 在 Android 上运行示例应用
npx expo run:android
# 在 iOS 上运行示例应用
npx expo run:ios

您将在启动示例应用时在屏幕中央看到文本“主题:系统”。值“system”来自于同步调用原生模块中的 getTheme() 函数。您将在下一步中更改该值。

4

获取、设置和持久化主题偏好值

Android 原生模块

要读取值,请查找键为“theme”的 SharedPreferences 字符串。如果键不存在,则默认值为“system”。使用 reactContext(一个 React Native ContextWrapper)通过 getSharedPreferences() 访问 SharedPreferences 实例。

要设置该值,请使用 SharedPreferencesedit() 方法获取 Editor 实例。然后,使用 putString() 设置值。确保 setTheme 函数接受类型为 String 的值。

android/src/main/java/expo/modules/settings/ExpoSettingsModule.kt
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”。

要设置该值,请使用 UserDefaultsset(_:forKey:) 方法。确保 setTheme 函数接受类型为 String 的值。

ios/ExpoSettingsModule.swift
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.tsExpoSettingsModule 原生模块添加 TypeScript 接口以更新主题。

src/ExpoSettingsModule.ts
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 调用您的原生模块。

src/index.ts
import ExpoSettingsModule from './ExpoSettingsModule'; export function getTheme(): string { return ExpoSettingsModule.getTheme(); } export function setTheme(theme: string): void { return ExpoSettingsModule.setTheme(theme); }

示例应用

您现在可以在示例应用中使用设置 API。

example/App.tsx
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 函数创建。

android/src/main/java/expo/modules/settings/ExpoSettingsModule.kt
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/ExpoSettingsModule.swift
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 模块

src/ExpoSettings.types.ts
export type ThemeChangeEvent = { theme: string; }; export type ExpoSettingsModuleEvents = { onChangeTheme: (params: ThemeChangeEvent) => void; };
src/index.ts
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.tsx
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 时容易发生错误,因为它允许任何字符串值。通过使用枚举来限制可能的值为 systemlightdark,提高该 API 的类型安全性。

Android 原生模块

android/src/main/java/expo/modules/settings/ExpoSettingsModule.kt
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/ExpoSettingsModule.swift
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 模块

src/ExpoSettings.types.ts
export type Theme = 'light' | 'dark' | 'system'; export type ThemeChangeEvent = { theme: Theme; }; export type ExpoSettingsModuleEvents = { onChangeTheme: (params: ThemeChangeEvent) => void; };
src/ExpoSettingsModule.ts
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');
src/index.ts
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 枚举的有效值。只有有效值为 lightdarksystem

恭喜!您已为 Android 和 iOS 创建了您的第一个 Expo 模块。

后续步骤

Expo 模块 API 参考

使用 Kotlin 和 Swift 创建原生模块。

教程:创建原生视图

使用 Expo 模块 API 创建原生视图的教程。