使用 Kotlin 和 Swift 为 React Native 构建原生模块 (翻译转载)

 

 

2023-02-02679阅读12分钟
 

忘记Java和Objective-C。相反,学习如何使用 Kotlin 和 Swift,使用带有 promise 的原始和复杂数据类型与 React Native 应用程序的原生层进行通信。

作者:Jefferson Tavares de Pádua,Whitespectre 的高级 React Native 开发人员

在本文中,您将学习如何:

  • 使用打字稿作为构建本机模块的起点
  • 在 React Native 中使用 Kotlin 构建原生 Android 模块
  • 在 React Native 中使用 Swift 构建原生 iOS 模块

大量可用于 Android 和 iOS 的库提供了一种使用 React Native 与其功能交互的方法(正式或通过社区管理的包)。今天,大多数关于创建本机模块的信息都是使用Java和Objective-C来实现的。

然而,Kotlin 和 Swift 实际上是解决这个问题的更现代的方法,因为与它们的对应物(例如:Kotlin 数据类与 POJO 的数据类)相比,它们提供了不那么冗长和对开发人员更友好的语法,并且通常性能更高,资源消耗更少(例如:Swift 的延迟初始化)。

因此,在本文中,您将学习如何使用 Kotlin 和 Swift 创建自己的原生模块。我们将超越基本步骤,具体介绍如何通过 React Native 桥传递原始和复杂的数据类型,以及如何在原生 Android 和 iOS 层上处理 Promises。准备?让我们开始吧。

我们将使用 Typescript、Kotlin 和 Swift 构建什么

我们在这里的主要目标是向您展示如何构建本机模块来包装第三方库的功能。

为了做到这一点,我们将模拟我们公开一个模块的功能,该模块在给定跟踪 ID 和用户的唯一代码的情况下跟踪包。

出于显而易见的原因,我们不会涉足使用谷歌地图,GPS位置或类似的东西,我们将简单地公开本机库的功能。但是,当然,如果您正在寻找如何将没有本机绑定的第三方本机Android / iOS库集成到React Native以在项目中使用,则需要执行这些步骤。

由于实际的包跟踪器需要与后端进行某种通信,我们将使用 JavaScript Promise 从用 Kotlin 和 Swift 编写的原生层接收数据,并使用 JavaScript 对象将数据从 React Native 传递到原生 Android 和 iOS 层以执行操作。我们将只关注 Typescript 与 Kotlin/Swift 之间的通信,而不是构建任何与 UI 相关的组件,毕竟,这就是您最终来到这里的原因,对吧?

所以让我们开始编码。

使用打字稿作为起点

创建本机模块时,最好有一个 Typescript 定义,说明该模块在请求该模块的任何地方的外观。例如,我们的 TS 定义将如下所示:

 
js
体验AI代码助手
代码解读
复制代码
    import { NativeModules } from 'react-native';
 
js
体验AI代码助手
代码解读
复制代码
    export interface TrackPackageRequest {  
    id: string;  
    verificationCode: string;  
    }  
    export interface TrackPackageResult {  
    request: TrackPackageRequest;  
    distance: number;  
    status: 'moving' | 'delivered';  
    }  
    export interface PackageTrackerModule {  
    track: (request: TrackPackageRequest) => Promise<TrackPackageResult>;  
    }  
    export default NativeModules.PackageTrackerModule as PackageTrackerModule;

让我们详细介绍一下这里发生的事情:

  • 我们首先要做的是导入,我们将 Native Module 常量从 React Native 引入。该常量包含对使用应用程序初始化的所有模块的引用,稍后我们将使用它。
  • 从那里,我们有接口 TrackPackageRequest 和 TrackPackageResult 分别**定义了我们应该发送到本机模块的内容以及我们可以期望返回的响应。
  • 然后,我们有 PackageTrackerModule 接口来定义模块的实际形状,我们将使用它来转换 NativeModules.PackageTrackerModule 的值,以便我们可以剥离 Typescript 的所有好处,即使在与我们的本机模块交互时也是如此。**

免责声明:你必须确保接口上可用的方法在原生层上有一个匹配的绑定(本质上是一个具有相同名称、参数和返回类型的方法),否则从 React Native 调用模块时会遇到问题。PackageTracker

现在我们已经整理了 Typescript 端,让我们深入了解本机层以响应对我们在此处公开的方法的调用。track

在 React Native 中使用 Kotlin 构建原生 Android 模块

自从谷歌宣布Kotlin将被正式支持作为Android的开发语言以来,它的受欢迎程度飙升,其背后的原因非常清楚:

Kotlin 是一种易于使用的现代编程语言,具有出色的标准库(而大多数使用 Java 的 Android 开发人员仍然坚持使用 Java 6/7),并且开箱即用即可与 Java 完全互操作,因此 Kotlin 是我们将用于公开模块的 Android 原生功能的语言。

首先,让我们创建一个名为的文件,在该文件中,我们将添加一个同名的类,该类从 .PackageTrackerModule.kt ReactContextBaseJavaModule

这是在本机层和 TS 层之间创建链接的重要部分,因此让我在这里为您提供有关幕后发生的事情的更多详细信息。

当您扩展类时,Android Studio 将提示您实现一个名为 的方法,请这样做并返回为 .ReactContextBaseJavaModule getName PackageTrackerModule String

请记住,在我们的 TS 文件中,我们导入了常量并使用它来导出一个名为 ?好吧,我们从此处的方法返回相同的值并非巧合。这就是 React Native 将用来识别我们在 TS 层使用方法时需要调用哪些功能的内容,因此,如果您要为其他内容创建模块,请留意正在使用的名称。NativeModules PackageTrackerModule getName

请记住,在我们的 TS 文件中,我们导入了常量并使用它来导出一个名为 ?好吧,我们从此处的方法返回相同的值并非巧合。这就是 React Native 将用来识别我们在 TS 层使用方法时需要调用哪些功能的内容,因此,如果您要为其他内容创建模块,请留意正在使用的名称。NativeModules PackageTrackerModule getName

 
js
体验AI代码助手
代码解读
复制代码
    class PackageTrackerModule : ReactContextBaseJavaModule() {  
        override fun getName(): String {  
        return "PackageTrackerModule"  
        }  
    }

现在我们有了我们的模块,让我们创建一个 React 包来与之关联。

创建另一个调用的文件,然后包含从 扩展的具有相同名称的类。执行此操作后,系统将提示你重写两种方法的实现。我们将使用该方法创建在上一步中创建的类的实例。PackageTrackerReact.kt ReactPackage createNativeModules PackageTrackerModule

 
js
体验AI代码助手
代码解读
复制代码
    class PackageTrackerReact : ReactPackage {  
        override fun createNativeModules(context: ReactApplicationContext): MutableList<NativeModule> {  
        return mutableListOf(PackageTrackerModule())  
        }
 
js
体验AI代码助手
代码解读
复制代码
    override fun createViewManagers(context: ReactApplicationContext): MutableList<ViewManager<View, ReactShadowNode<*>>> {  
      return mutableListOf()  
     }  
    }

原生 Android 层上该过程的最后一步是将我们的包链接到我们的应用程序。为此,请转到您的文件并找到一个调用的方法,其中包含要返回的包列表。我们在这里要做的是传递我们包的一个实例,以便 React Native 知道它的存在并将其作为 TS 层中的 prop 返回:MainApplication getPackages NativeModules

 
js
体验AI代码助手
代码解读
复制代码
    @Override  
    protected List<ReactPackage> getPackages() {  
    @SuppressWarnings("UnnecessaryLocalVariable")  
    List<ReactPackage> packages = new PackageList(this).getPackages();
 
js
体验AI代码助手
代码解读
复制代码
    packages.add(new PackageTrackerReact()); // This line  
    return packages;  
    }

联动现在全部完成!

从这里开始,我们要开始向模块添加功能,所以让我们回到类并创建我们的方法:PackageTrackerModule track

 
js
体验AI代码助手
代码解读
复制代码
    @ReactMethod  
    fun track(data: ReadableMap, promise: Promise) {  
    val id = data.getString("id")  
    val verificationCode = data.getString("verificationCode")
 
js
体验AI代码助手
代码解读
复制代码
    if(id == null || verificationCode == null) {  
    promise.reject("PACKAGE_NOT_FOUND", "Id and Verification code didn't match a package")  
    return  
    }  
    val handler = Handler(Looper.getMainLooper())  
    handler.postDelayed({  
    val matchingPackage = packages.find { it.id == id && it.verificationCode == verificationCode }  
    if (matchingPackage == null) {  
    promise.reject("PACKAGE_NOT_FOUND", "Id and Verification code didn't match a package")  
    return@postDelayed  
    }  
    val response = Arguments.createMap().apply {  
    putMap("request", data)  
    putInt("distance", matchingPackage.distance)  
    putString("status", matchingPackage.status)  
    }  

    promise.resolve(response)  
    }, 5000)  
    }

等!不要害怕 😅

此代码的大部分与您自己的用例无关。它更多的是模仿第三方包的行为。

让我们来看看您将每天使用的实际相关部分:

  • 首先是注释。该注释公开了可以从 React Native 调用的方法,因此,如果要创建一个仅用于包内内部使用的方法,请不要在该方法中包含注释。@ReactMethod
  • 其次,方法签名:如您所见,它看起来与我们在 TS 模块中定义的并不完全相同,对吗?嗯,这是因为你从 React Native 发送到原生 Android/iOS 层的所有数据都需要转换,而在 Android 上,你的对象参数将被转换为 .ReadableMap

P/S:如果您要发送原语(例如:字符串、布尔值等),则可以使用 Kotlin 中的字符串或布尔类型直接获取它们的值。

同样重要的是要注意跟踪方法中的第二个参数,这是我们在 TS 定义中也没有定义的 Promise。

该值实际上将由 React Native 在内部注入,因此当您调用本机模块时,您可以使用它将某些内容返回给 TS 层。为此,您将分别调用 promise.resolve 和 promise.reject 方法,只要您的操作完成并成功或错误。

但是,在模块中传递 promise 对象时要小心,因为一个 promise 只能解析/拒绝一次,所以如果你尝试在多个地方解析/拒绝同一个 promise,它会让你的应用崩溃。

除此之外,这里唯一值得一提的是使用 .正如我之前提到的,在应用程序的不同层之间传递的数据需要转换,因此我们将使用它从本机层返回映射(它几乎映射到另一端的常规 JavaScript 对象)。Arguments.createMap()

有了这个,我们的原生模块就可以在 Android 设备上运行时从 React Native 调用了!

现在让我们看看需要做些什么才能使此功能在 iOS 上也可用。

在 React Native 中使用 Swift 构建原生 iOS 模块

现在是时候在我们应用程序的 iOS 端实现该功能了。我们将使用 Swift 作为我们的主要编程语言,因为它提供了比 Objective-C 更具可读性的语法。这次我们将直接进入它,因为我们之前在 TS 层上所做的所有事情仍然有用,因此我们不必再次编写它们。

当我们计划用 Swift 编写实际代码时,我们必须首先在 Objective-C 中创建一个接口来映射我们的模块。这就是为什么我之前说过 Swift 将成为我们的主要编程语言,因为到目前为止,没有办法避免编写一些 Objective-C 来与 React Native 应用程序的原生层进行交互。但不用担心,我们会让事情变得简单。创建一个调用的文件,然后添加以下内容:PackageTrackerModule.m

 
js
体验AI代码助手
代码解读
复制代码
    #import <Foundation/Foundation.h>  
    #import <React/RCTBridgeModule.h>
 
js
体验AI代码助手
代码解读
复制代码
    @interface RCT_EXTERN_MODULE(PackageTrackerModule, NSObject)RCT_EXTERN_METHOD(track: (NSDictionary)data resolver: (RCTPromiseResolveBlock)resolve rejecter: (RCTPromiseRejectBlock)  
    reject)  
    @end

如您所见,我们正在使用宏来定义我们的接口,以便 React Native 知道我们模块的存在。RCT_EXTERN_MODULE

然后,我们使用宏来注册我们的跟踪方法,以便也可以从 TS 层调用它。RC_EXTERN_METHOD

与 Android 层上发生的情况类似,我们也以与从 Typescript 调用函数时所期望的方式略有不同的方式传递参数,因此以下是它的工作原理:

NSDictionary是React Native类型转换我们的TS对象(你可以把它想象成我们在Android上看到的ReadableMap),并且和与Android层上的Promise对象相同,唯一的区别是在iOS上不是将它们捆绑到单个对象中,而是自动注入单独的方法供我们使用。RCTPromiseResolveBlock``RCTPromiseRejectBlock

接下来,让我们使用 Swift 创建此接口的实际实现:

 
js
体验AI代码助手
代码解读
复制代码
    @objc(PackageTrackerModule)  
    class PackageTrackerModule : NSObject {  
    @objc  
    func track(_ data: NSDictionary, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock ) {}  
    }

对于类的实际实现,要考虑的主要事情是在类和方法中使用的注释。您可以将这些视为我们在上一个类中定义的接口与 Swift 代码的具体实现之间的绑定。这些参数本质上是我们在界面上定义的 1:1 映射,因此无需在此处重复我自己。@objc

如果你对Objective-C不是很有经验,这里有一些事情需要注意,它很容易让你的应用程序崩溃。

看到解析器和拒绝器前缀了吗?这些是 Objective-C 层的命名参数,因此它们必须与我们界面上定义的名称完全匹配(回头看看 Objective-C 文件,你会看到它们是相同的名称),而且这些值的填充顺序与接口上定义的顺序相同, 因此,不要在接口上以一种方式定义它,而在类上以另一种方式定义它,否则您的应用程序将崩溃。

现在让我们将我们的实现添加到 track 方法中:

 
js
体验AI代码助手
代码解读
复制代码
    @objc func track(_ data: NSDictionary, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) {  
    guard let id = data[“id”] as? 字符串,让验证码=数据[“验证码”]作为? 字符串else {  
    reject(“PACKAGE_NOT_FOUND”, “Id 和验证码不匹配包”, nil)  

    return  
    }  

    let matchingPackage = packages.first{ $0.id == id && $0.verificationCode == verificationCode }  

    DispatchQueue.main.asyncAfter(deadline: .now() + 5) { if let match = matchingPackage { resolve([“request”: data, “distance”: matched.distance, “status”: matched.status])  
    } else {  


    reject(“PACKAGE_NOT_FOUND”, “Id 和验证码不匹配包裹”,零)  
    }  
    }  
    }

同样,您可以忽略此处的大部分代码,因为在实现自己的本机模块时不需要它。

这里相关的是如何执行与本机层的通信,所以让我们进入它:

正如我之前提到的,从 React Native 传递的数据以 NSDictionary 的形式接收,我们可以使用对象中的键来访问各个属性。

solve函数还需要一个NSDictionary,其中包含你想要传递回React Native层的数据,所以我们使用与Android上相同的数据创建它,而拒绝函数需要与我们的Android实现相比,NSError类型的额外参数。我们将 nil 作为拒绝函数最后一个参数的值传递,因为上述情况是预期结果,但在与第三方库集成时,如果内部发生异常,这是您用来将错误信息传递回 Typescript 的参数。

和。。。就是这样!

对于 iOS,无需创建 ReactPackage 或类似的东西,当我们使用宏定义模块的接口时,这一切都会自动注册。RCT_EXTERN_MODULE

有了这个,你现在有一个自定义的原生模块,可以从 React Native 调用,并直接与应用程序的 Android 和 iOS 层进行交互。您可能还想创建一个用户界面来查看此代码的运行情况,但老实说,这是一个相当高级的主题,因此如果您最终来到这里,您可能已经知道如何执行此操作。

无论哪种方式,我们都花时间整理了一个非常简单的界面来展示这个模块的实际效果,所以如果你想试一试,这里是 github 存储库的链接

posted on 2025-05-08 17:13  漫思  阅读(57)  评论(0)    收藏  举报

导航