Skip to main content

View boilerplate

Create package structure

Package boilerplate

package.json

Let's start by creating native-list-package directory (in project's root) containing package's code.

Inside that directory, let's create a package.json file with the following content:

package.json
{
"private": true,
"name": "native-list-package",
"version": "0.0.1",
"description": "My awesome package",
"react-native": "src",
"source": "src",
"main": "src",
"module": "src",
"files": [
"src",
"android",
"ios",
"NativeListPackage.podspec",
"!android/build",
"!ios/build",
"!**/__tests__",
"!**/__fixtures__",
"!**/__mocks__"
],
"repository": "<repository-url>",
"author": "<author>",
"license": "MIT",
"homepage": "<homepage-url>",
"peerDependencies": {
"react": "*",
"react-native": "*"
},
"codegenConfig": {
"name": "NativeListPackage",
"type": "all",
"jsSrcsDir": "src",
"android": {
"javaPackageName": "com.nativelistpackage"
}
}
}

For codegenConfig, you can check out RN's Configure Codegen docs section.

In this case, we want to code-generate the package with name NativeListPackage with JS specs in src directory and com.nativelistpackage Android package name.

NativeListPackage.podspec

Next, let's create NativeListPackage.podspec, the "equivalent" of package.json, but for CocoaPods packages:

NativeListPackage.podspec
# `.podspec` file is like "`package.json`" for iOS CocoaPods packages

require "json"

package = JSON.parse(File.read(File.join(__dir__, "package.json")))

# Detect if new arch is enabled
new_arch_enabled = ENV['RCT_NEW_ARCH_ENABLED'] == '1'

Pod::Spec.new do |s|
s.name = "NativeListPackage"
s.version = package["version"]
s.summary = package["description"]
s.description = package["description"]
s.homepage = package["homepage"]
s.license = package["license"]
s.platforms = { :ios => "13.0" }
s.author = package["author"]
s.source = { :git => package["repository"], :tag => "#{s.version}" }

# This is crucial - declare which files will be included in the package (similar to "files" field in `package.json`)
s.source_files = "ios/**/*.{h,m,mm,swift}"

if new_arch_enabled
s.pod_target_xcconfig = {
"DEFINES_MODULE" => "YES",
"SWIFT_OBJC_INTERFACE_HEADER_NAME" => "NativeListPackage-Swift.h",
# This is handy when we want to detect if new arch is enabled in Swift code
# and can be used like:
# #if NATIVE_LIST_PACKAGE_NEW_ARCH_ENABLED
# // do sth when new arch is enabled
# #else
# // do sth when old arch is enabled
# #endif
"OTHER_SWIFT_FLAGS" => "-DNATIVE_LIST_PACKAGE_NEW_ARCH_ENABLED"
}
else
s.pod_target_xcconfig = {
"DEFINES_MODULE" => "YES",
"SWIFT_OBJC_INTERFACE_HEADER_NAME" => "NativeListPackage-Swift.h"
}
end

# Install all React Native dependencies (RN >= 0.71 must be used)
#
# check source code for more context
# https://github.com/facebook/react-native/blob/0.71-stable/scripts/react_native_pods.rb#L172#L180
install_modules_dependencies(s)
end

This will link Objective-C, Objective-C++ & Swift files from ios directory and link iOS RN dependencies.

build.gradle

We also need similar configuration for Android. First let's create android directory and then under that directory create build.gradle file.

android/build.gradle
buildscript {
ext.safeExtGet = {prop, fallback ->
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}

def kotlin_version = safeExtGet('kotlinVersion', '1.6.10') // Mandatory, if you will use Kotlin

repositories {
google()
gradlePluginPortal()
}
dependencies {
classpath("com.android.tools.build:gradle:7.2.1")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") // Mandatory, if you will use Kotlin
}
}

def isNewArchitectureEnabled() {
return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
}

apply plugin: 'com.android.library'
apply plugin: 'kotlin-android' // Mandatory, if you will use Kotlin
if (isNewArchitectureEnabled()) {
apply plugin: "com.facebook.react"
}

android {
compileSdkVersion safeExtGet('compileSdkVersion', 33)

namespace "com.nativelistpackage"

defaultConfig {
minSdkVersion safeExtGet('minSdkVersion', 21)
targetSdkVersion safeExtGet('targetSdkVersion', 33)
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
}

sourceSets {
main {
if (isNewArchitectureEnabled()) {
java.srcDirs += ['src/newarch/java', "${project.buildDir}/generated/source/codegen/java"]
} else {
java.srcDirs += ['src/oldarch/java']
}
}
}
}

repositories {
maven {
url "$projectDir/../node_modules/react-native/android"
}
mavenCentral()
google()
}

apply from: "$projectDir/react-native-helpers.gradle"

dependencies {
if (project.ext.shouldConsumeReactNativeFromMavenCentral()) {
implementation "com.facebook.react:react-android" // Set by the React Native Gradle Plugin
} else {
implementation 'com.facebook.react:react-native:+' // From node_modules
}
}

This will link Java & Kotlin files under android/src/{main|newarch|oldarch}/java/com/<packagename> as well as code-generated files under android/build/generated/source/codegen/java/com/<packagename> (combined values declared under namespace and sourceSets).

You may have noticed, that the configuration loads sth from $projectDir/react-native-helpers.gradle. In fact that file includes implementation for shouldConsumeReactNativeFromMavenCentral helper. Let's create it:

android/react-native-helpers.gradle
def safeAppExtGet(prop, fallback) {
def appProject = rootProject.allprojects.find { it.plugins.hasPlugin('com.android.application') }
appProject?.ext?.has(prop) ? appProject.ext.get(prop) : fallback
}

// Let's detect react-native's directory, it will be used to determine RN's version
// https://github.com/software-mansion/react-native-reanimated/blob/cda4627c3337c33674f05f755b7485165c6caca9/android/build.gradle#L88
def resolveReactNativeDirectory() {
def reactNativeLocation = safeAppExtGet("REACT_NATIVE_NODE_MODULES_DIR", null)
if (reactNativeLocation != null) {
return file(reactNativeLocation)
}

// monorepo workaround
// react-native can be hoisted or in project's own node_modules
def reactNativeFromProjectNodeModules = file("${rootProject.projectDir}/../node_modules/react-native")
if (reactNativeFromProjectNodeModules.exists()) {
return reactNativeFromProjectNodeModules
}

def reactNativeFromNodeModules = file("${projectDir}/../../react-native")
if (reactNativeFromNodeModules.exists()) {
return reactNativeFromNodeModules
}

throw new GradleException(
"[native-list-package] Unable to resolve react-native location in " +
"node_modules. You should project extension property (in app/build.gradle) " +
"`REACT_NATIVE_NODE_MODULES_DIR` with path to react-native."
)
}

// https://github.com/software-mansion/react-native-reanimated/blob/cda4627c3337c33674f05f755b7485165c6caca9/android/build.gradle#L199#L205
def reactNativeRootDir = resolveReactNativeDirectory()

def reactProperties = new Properties()
file("$reactNativeRootDir/ReactAndroid/gradle.properties").withInputStream { reactProperties.load(it) }

def REACT_NATIVE_VERSION = reactProperties.getProperty("VERSION_NAME")
def REACT_NATIVE_MINOR_VERSION = REACT_NATIVE_VERSION.startsWith("0.0.0-") ? 1000 : REACT_NATIVE_VERSION.split("\\.")[1].toInteger()

project.ext.shouldConsumeReactNativeFromMavenCentral = { ->
return REACT_NATIVE_MINOR_VERSION >= 71
}

Wow, lots of code just to detect used RN version. Fortunately, you will only have to write it once.

If you are curious, this code tries to locate react-native/ReactAndroid/gradle.properties file, where VERSION_NAME property is saved. Then it tries to retrieve minor part and use it in shouldConsumeReactNativeFromMavenCentral. That function checks if minor version is greater or equal than 71 (the first version with RN's Android code published to MavenCentral - sth like npm, but for Android packages). The function is declared as project.ext/by extra, which makes it possible to use that in other Gradle script file.

Source files stubs

Finally, let's create stubs for source files. For JS, create following empty files:

  • src/index.ts
  • src/NativeListView.tsx
  • src/NativeListView.android.tsx
  • src/NativeListView.ios.tsx
  • src/AndroidNativeListViewNativeComponent.ts
  • src/RNNativeListViewNativeComponent.ts

For iOS implementation, create:

  • ios/DataItem.swift
  • ios/NativeListCell.swift
  • ios/RNNativeListViewComponentView.h
  • ios/RNNativeListViewComponentView.mm
  • ios/RNNativeListViewManager.h
  • ios/RNNativeListViewManager.mm
  • ios/RNNativeListViewContainerView.swift
  • ios/RNNativeListViewViewController.swift
  • ios/NativeListPackage-Bridging-Header.h

For Android implementation, create:

  • android/src/main/java/com/nativelistpackage/AndroidNativeListViewFragment.kt
  • android/src/main/java/com/nativelistpackage/NativeListTurboPackage.kt
  • android/src/main/java/com/nativelistpackage/DataItem.kt
  • android/src/main/java/com/nativelistpackage/NativeListAdapter.kt
  • android/src/main/java/com/nativelistpackage/NativeListViewHolder.kt
  • android/src/main/res/layout/card_item.xml
  • android/src/main/res/layout/fragment_list.xml
  • android/src/newarch/java/com/nativelistpackage/AndroidNativeListViewManager.kt
  • android/src/oldarch/java/com/nativelistpackage/AndroidNativeListViewManager.kt

To make the codegen work on Android, the instance of TurboReactPackage needs to be found by the React Native CLI. So let's fill the minimal boilerplate to make it possible to link the package.

android/src/main/java/com/nativelistpackage/NativeListTurboPackage.kt
package com.nativelistpackage

import com.facebook.react.TurboReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider
import com.facebook.react.turbomodule.core.interfaces.TurboModule

class NativeListTurboPackage : TurboReactPackage() {
/**
* Initialize and export modules based on the name of the required module
*/
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
return null
}

/**
* Declare info about exported modules
*/
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
/**
* Here declare the array of exported modules
*/
val moduleList: Array<Class<out NativeModule?>> = arrayOf(
)
val reactModuleInfoMap: MutableMap<String, ReactModuleInfo> = HashMap()
/**
* And here just iterate on that array and produce the info provider instance
*/
for (moduleClass in moduleList) {
val reactModule = moduleClass.getAnnotation(ReactModule::class.java) ?: continue
reactModuleInfoMap[reactModule.name] =
ReactModuleInfo(
reactModule.name,
moduleClass.name,
true,
reactModule.needsEagerInit,
reactModule.hasConstants,
reactModule.isCxxModule,
TurboModule::class.java.isAssignableFrom(moduleClass)
)
}
return ReactModuleInfoProvider { reactModuleInfoMap }
}
}

To link the package without adding it to node_modules, we will use react-native.config.js

react-native.config.js

Go to react-native.config.js (create it, if needed) inside root directory of your app and add native-list-package under dependencies property:

react-native.config.js
const path = require('path');

module.exports = {
dependencies: {
'native-list-package': { // <--------- Add entry for "native-list-package"
root: path.resolve(__dirname, './native-list-package'),
},
},
};

babel.config.js

Go to babel.config.js and add native-list-package under alias property of babel-plugin-module-resolver:

babel.config.js
module.exports = {
presets: [ 'module:metro-react-native-babel-preset' ],
plugins: [
[
'module-resolver',
{
root: [ './' ],
extensions: [
'.ios.js',
'.ios.ts',
'.ios.tsx',
'.android.js',
'.android.ts',
'.android.tsx',
'.js',
'.ts',
'.tsx',
'.json',
],
+ alias: {
+ 'native-list-package': './native-list-package'
+ }
},
],
],
};

tsconfig.json

Go to tsconfig.json and add app-info-package under compilerOptions.paths field:

tsconfig.json
{
"extends": "@tsconfig/react-native/tsconfig.json", /* Recommended React Native TSConfig base */
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */

/* Completeness */
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
+ "paths": {
+ "native-list-package": ["./native-list-package"],
+ }
}
}

[iOS] Pods installation

Install Pods to link them to iOS project.

npx pod-install
tip
You might encounter following error:
Specs satisfying the NativeListPackage (from "../native-list-package") dependency were found, but they required a higher minimum deployment target.

To fix it, go to project's Podfile at <rootDir>/ios/Podfile and change platform :ios, min_ios_version_supported to platform :ios, '13.0'

Setup completed. Now, let's start coding with the JS spec.