Skip to content

Latest commit

 

History

History
196 lines (154 loc) · 11.3 KB

NATIVE_MODULE_DEVELOPING.md

File metadata and controls

196 lines (154 loc) · 11.3 KB

Native Developing

Adding new Native Modules

For creating a new package from scratch we can simple run a few commands to start having an skeleton of what is going to be our future package.

npx create-react-native-library@latest <package-name> --local

Official documentation about create-react-native-library can be found here.

Notice the --local flag that is used to create this skeleton. Without this the library will introduce also a React Native app as example when generating the skeleton. Since are not interested on it we have to make sure this command is ran using this flag.

After running this, the new skeleton should look like the following:

packages/
└── <package-name>/
    ├── ios/                          # containing an Xcode workspace/project
    ├── android/                      # containing an Android workspace/project
    ├── src/
    │   ├── __tests__/
    │   └── index.ts
    ├── tsconfig.json
    ├── package.json
    ├── .eslintrc.js
    ├── README.md
    └── RNEmbrace<PackageName>.podspec    # `RNEmbrace` is the prefix as per convention. Make sure the `.podspec` file generated by `create-react-native-library` is renamed following this convention. Also make sure to update the content of this file to rename the value of `s.name`.

Make sure the new package.json file list all files/folders we want to get packed. Notice that in the following example android has a very detailed list. That's the minimum the Android Native module needs for building/running into a React Native application.

{
  "files": [
    "lib",
    "android/src",
    "android/build.gradle",
    "android/dependencies.gradle",
    "android/gradle.properties",
    "ios",
    "RNEmbrace<PackageName>.podspec"
  ],
}

This example is configured to pack lib/, android/, ios/ and RNEmbrace.podspec into the package we will publish in the future. All folders/files that should be packed and published should be listed here. If they are not there, the pack/publish process will ignore them.

Notice that this command will generate a basic structure for each platform, but we also need to tweak certain file names and contents to properly customize the new package following the Embrace conventions.

iOS Native Module

References are very important when developing a new iOS Standalone Native Module, we encourage developers to create new modules through Xcode to avoid any issue related to this.

Recommended steps

  • Open Xcode and create a project: File -> New -> Project -> Static Library (for src files) into packages/PACKAGE_NAME/ios. This will create the RNEmbrace__NAME__.xcodeproj file and its respective configuration.
  • Create a new target with the same name.
  • Proper source files are supposed to be created by the create-react-native-library library following the iOS Native Modules for React Native official documentation. We are supposed to link them into the new project.

This repository already contains classes using Swift. We highly recommend to keep this approach. More information about how to do it can be found in the official documentation.

At the end of this process the ios folder structure should contain something like the following:

ios/
└── RNEmbrace<PackageName>/
    ├── RNEmbrace<PackageName>.xcodeproj
    ├── RNEmbrace<PackageName>-Bridging-Header.h
    ├── RNEmbrace<PackageName>.m
    └── RNEmbrace<PackageName>.swift

This is the bare minimum we need to create a new iOS Native Module.

The proper .podspec file outside the ios folder listing all dependencies should be already created previously by running the command from create-react-native-library. It's a good idea to check that this file is in place after running the build and pack the new package (without this file the new iOS Native Module won't be recognized by the application and won't be installed).

Testing

  • Under packages/new-package create a test-project directory.
test-project /
└── android
└── ios
    └── Podfile // This should contain the proper React Native install script adding all dependencies we need
└── package.json
└── yarn.lock

Create Unit Test suite for the new iOS Native Module

  • Using XCode create a new Framework: Project -> File -> New -> Project -> Framework (iOS). a. Product Name -> RNEmbrace__NAME__ (no suffix Test here, should be the same name as the ios source package) b. Language -> Swift c. Team -> None d. Organization Identifier -> io.embrace.rnembrace__NAME__ e. Testing System -> XCTest
  • Save it into /test-project/ios (this will create a dir called RNEmbrace__NAME__, a RNEmbrace__NAME__.xcodeproj and a dir RNEmbrace__NAME__Tests).
  • Remove the /test-project/ios/RNEmbrace__NAME__/RNEmbrace__NAME__ dir, it's not needed (but keep the target).
  • Add a reference to the iOS source package in RNEmbrace__NAME__ root folder (project). Do not copy/move files. The reference is what we need here (Location: Relative to a Group) a. Action -> Reference files in place b. Groups -> Create Groups c. Targets -> both (regular and test targets)
  • Make sure to click into the new referenced folder, open the File Inspector (top right corner of XCode) and update the location of the folder to be relative to the project (instead of absolute). It should show a relative path like ../../../ios/RNEmbrace__NAME__
  • After all make sure to follow steps in (this comment)[CocoaPods/CocoaPods#12583 (comment)]
  • Move everything using XCode (because of references) from packages/__PACKAGE_NAME__/test-project/ios/RNEmbrace__NAME__/ to packages/__PACKAGE_NAME__/test-project/ios/ (we don't need to keep the RNEmbrace__NAME__ dir).
  • At this point the structure will be ios/.xcodeproj/ + ios/REmbraceTracerProviderTests/
  • Into the .xcodeproj dir run pod init to initialize the Podfile with the minimum targets configuration (this will create the ios/Podfile)
  • Add all required dependencies + React Native (this can be copied/pasted from already existent suites)
  • Run pod install to install the dependencies + create the RNEmbrace__NAME__.xcworkspace (do not create it manually since it is going to require extra setup we don't want to go over).
  • Run yarn install:ios after creating the proper script in packages//package.json (take a look at other packages as references).
  • This should install all dependencies related to React Native and those coming from Embrace (listed in ios/Podfile).

Some reminders

  • Make sure it is created the RNEmbrace__NAME__Tests.xctestplan into test-project/ios by opening Edit Scheme (top center of xcode) -> look for Tests Plan -> Click on the arrow at the right of the name. If it is the first time you click on it and the RNEmbrace__NAME__Tests.xctestplan file was not saved yet, it will ask for it. Save it, so the changes will persist. Otherwise the configurations will be lost.

  • Make sure swift classes add the React import at the top of the file.

  • Make sure BUILD_LIBRARY_FOR_DISTRIBUTION / Build Libraries for Distribution is set to No for regular target (not the test one).

  • Make sure ENABLE_USER_SCRIPT_SANDBOXING / User Script Sandboxing is set to NO for regular target (not the test one).

  • Make sure to run rm .xcode.env.local is part of the ios:install script on each package (more info about it can be found (here)[facebook/react-native#36762 (comment)]).

  • At this point the structure should look similarly to

test-project/
└── ios
    ├── RNEmbrace<PackageName>/ (just the reference)
    ├── RNEmbrace<PackageName>Tests/
    │   └── RNEmbrace<PackageName>Tests.swift
    ├── RNEmbrace<PackageName>Tests.xcworkspace
    ├── RNEmbrace<PackageName>Tests.xcodeproj // created manually
    └── Podfile
└── package.json
└── yarn.lock
  • Notice that RNEmbrace<PackageName> and RNEmbrace<PackageName>Tests are the Framework + XCTest targets created by xcode.

Tests can be run from XCode by opening test-project/ios/RNEmbraceTests.xcworkspace as a Workspace running the Test target or from the CLI with yarn ios:test

Android Native Module

  • Using Android Studio for creating a new project: File -> New -> New Project -> Empty Activity + kotlin (into package/<package-name>test-project/android dir). Make sure there is support for JUnit tests.
  • Rename the settings.gradle.kts -> to just settings.gradle (just for consistency, other packages has this file already written in groovy).
  • Add React Native configuration (this can be copied/pasted from other existent packages like @embrace-io/react-native-tracer-provider)
  • Make sure includeBuild("../node_modules/@react-native/gradle-plugin") is added into pluginManagement
  • Include the local package we want to test (include ':react-native-<package-name>')
  • Link the local package into test (project(':react-native-tracer-provider').projectDir = file('../../android'))
  • Update rootProject.name (rootProject.name = "io.embrace.rnembrace<packagename>test")
  • Into the gradle.properties -> Add custom properties (particularly RNEmbrace<PackageName>_packageJsonPath following other packages as example)
  • Add android/config folder with respective content (detekt plugin for linting, in the future this is going to be moved at the root of the repo avoiding config duplication)

Some reminders

  • Remove app/src/androidTest
  • Remove app/src/main/java
  • Remove all app/src/main/res except for app/src/main/res/values/strings.xml and app/src/main/AndroidManifest.xml
  • Rename app/src/test/java/io/embrace/xxxx/ExampleUnitTest.kt to RNEmbrace__NAME__Test.kt
  • Replace content of app/build.gradle.kts with proper configuration (using as example what other packages)
  • Make sure app/build.gradle.kts includes the right local package (implemented in settings.gradle)
  • Make sure apply("../../../android/dependencies.gradle") is added into app/build.gradle.kts
  • Make sure android/gradle/wrapper/gradle-wrapper.properties points to a gradle version we support (7.5.1 atm)
  • Make sure android/build.gradle includes an AGP we support (com.android.tools.build:gradle:7.4.2 atm)

Tests can be run from Android Studio by adding test-project/android as a Project or from the CLI with yarn android:test.

JS interface

If there are calls to NativeModules.<YourModule>.<method> that aren't explicitly covered by error handling then a wrapper module should be defined to give a better error message in the case that the package hasn't been installed correctly on the native side:

// packages/your-package/src/YourModule.ts

import {NativeModules, Platform} from "react-native";

const LINKING_ERROR =
  `The package '@embrace-io/your-package' doesn't seem to be linked. Make sure: \n\n` +
  Platform.select({ios: "- You have run 'pod install'\n", default: ""}) +
  "- You rebuilt the app after installing the package\n" +
  "- You are not using Expo Go\n";

export const YourModule = NativeModules.NativeModuleName
  ? NativeModules.NativeModuleName
  : new Proxy(
      {},
      {
        get() {
          throw new Error(LINKING_ERROR);
        },
      },
    );