React Native Bridge cover

Introduction

Today is the day – the day you write your first native module!

Are you feeling excited yet?

In this guide, I will show you how to bypass the complexities of C++ and instead use TypeScript to automate the code generation process. Together, we’ll create a basic app that retrieves the user’s contacts and list them. But before we delve into coding, it’s essential for you to grasp the concepts of Codegen and Turbo Module.

Let’s get started!

Codegen and Turbo Modules

Creating your own native module using C++ can be a frightful task. Recognizing this challenge, Facebook aimed to simplify the process of developing native bindings, leading to the creation of the enigmatic tool known as Codegen.

Codegen, a tool developed by Facebook, serves the purpose of generating multiple files written in C++, Objective-C++, and Java. These files establish a crucial connection between the JavaScript and native platforms, facilitated by the employment of the New Bridge. This mechanism ensures the maintenance of type consistency across all domains, freeing developers from the need to write long and repetitive code.

To put it simply, you only need to craft a single TypeScript type definition that outlines your module’s API. The rest? Consider it done!


A TurboModule is a specialized module that registers itself using JSI (JavaScript Interface) and is fully compatible with the Fabric architecture. Such modules are written in C++ and offer the advantage of bypassing the need for serializing and deserializing JSON during communication via the bridge.

While I haven’t introduced the concept of JSI yet, for now, you can think of a TurboModule as a module that aligns with the principles of the New Architecture. You can generate these modules using the Codegen tool or craft them manually using pure C++. In this part of the series, we will opt to generate our module using Codegen.

In summary, our approach involves utilizing a single TypeScript type to facilitate the generation of a TurboModule through the utilization of Codegen. It might sound a bit complex at first, but don’t worry – I’ll break it down step by step in this tutorial. By the end, you’ll have successfully developed your first TurboModule!

What we gonna build?

In our example we will build a simple app that will expose:

Info icon

In this tutorial, I will only be focusing on the New Architecture. Additionally, it’s worth mentioning that throughout this post, I will be constructing the happy path of the application, rather than developing the entire app with error handling, tests and UI that covers various permission states, pagination etc. I will delve into this concepts in Part 7 of this series.

Generate the project with react-native CLI

When it comes to building a native module, we have two options. We can either use a library generator like react-native-builder-bob, or create the module locally. Since we’re not planning to publish it to the npm registry, today we will be building a local module.

Keeping that in mind, let’s generate a new React Native project:

shell
npx react-native init rncontacts
Info icon

Before we start, I’d like to clean up the project a little bit. Personally, I’m not a big fan of Prettier and the default indentation. Feel free to skip this part if you prefer.

To disable Prettier and remove semicolons, open .eslint.rc and add the following lines:

eslint.rc
module.exports = {
    root: true,
    extends: '@react-native',
    rules: {
        'prettier/prettier': 0,
        semi: [2, 'never'],
    },
};

Additionally, add an .editorconfig file to maintain consistent indentation:

root = true

[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
max_line_length = 120
trim_trailing_whitespace = true

[*.json]
indent_style = space
indent_size = 2
Github icon

You can compare your changes by referring to this commit

Create Codegen spec

We will begin by creating our Codegen specification, which is a TypeScript type that conforms to the TurboModule interface. To organize our native module, let’s establish a folder in the project root with the following structure:

turbo-contacts/
└── spec/
    └── NativeTurboContacts.ts
Info icon

It’s important to use Native prefix for our specification file name. It’s required by codegen. If you fail to do so, your specifications will not be generated.

Open NativeTurboContacts.ts and include the following imports:

NativeTurboContacts.ts
import type { TurboModule } from 'react-native'
import { TurboModuleRegistry } from 'react-native'

Now, we need to create a TypeScript interface that extends the TurboModule type and outlines our native module’s structure. As you might recall from the section What we gonna build?, we need to expose three methods to access our phone’s contacts:

Let’s mirror these requirements in our TypeScript type:

NativeTurboContacts.ts
import type { TurboModule } from 'react-native'
import { TurboModuleRegistry } from 'react-native'

export type Contact = {
    firstName: string,
    lastName: string,
    phoneNumber: string | null
}

export interface Spec extends TurboModule {
    hasContactsPermission(): boolean,
    requestContactsPermission(): Promise<boolean>,
    getAllContacts(): Array<Contact>
}

Don’t overlook the export of our Contact type, which will be utilized in our view later on. The final part is to register our specification:

NativeTurboContacts.ts
import type { TurboModule } from 'react-native'
import { TurboModuleRegistry } from 'react-native'

export type Contact = {
    firstName: string,
    lastName: string,
    phoneNumber: string | null
}

export interface Spec extends TurboModule {
    hasContactsPermission(): boolean,
    requestContactsPermission(): Promise<boolean>,
    getAllContacts(): Array<Contact>
}

export default TurboModuleRegistry.getEnforcing<Spec>('TurboContacts')

We need to provide our module name to the getEnforcing function, so let’s use the name TurboContacts.

That’s nearly it!

In the same folder, let’s create an index.ts file and re-export our module for easier use in the application.

turbo-contacts/
└── spec/
    ├──  NativeTurboContacts.ts
    └── index.ts
index.ts
import TurboContactsModule, { Contact } from './NativeTurboContacts'

export const TurboContacts = TurboContactsModule
export type {
    Contact,
}

In our index file, we are simply importing both Contact type as well as our module. On line 3, we’re exporting TurboContacts , making it available for use in the entire app.

Generate files with Codegen

The question now is - how to generate C++ bindings based on our type? How will React Native recognize the presence of a folder named turbo-contacts with the Spec?

The truth is, it won’t 😔.

We need to establish a connection between the module and the original app. We have three options to achieve this:

  1. Install it using yarn add ./turbo_contacts (the easiest one, but we need to repeat it multiple times)
  2. Create a react-native.config.js configuration file in the project root (the best, but requires additional config)
  3. Link it using yarn link TurboContacts (improved step 1, but requires more setup)

In this tutorial, we will proceed with option one. If you’re interested in learning about how to utilize options 2 and 3, feel free to reach out to me on Twitter, and I’ll share a solution there.

We can’t actually install a package, without package.json. So let’s add one:

turbo-contacts/
└── spec/
    ├── NativeTurboContacts.ts
    └── index.ts
package.json

Open package.json file and add the following entries:

{
  "name": "TurboContacts",
  "version": "1.0.0",
  "peerDependencies": {
    "react": "*",
    "react-native": "*"
  }
}

Now, our module can be installed as a dependency of the rncontacts project. However, React Native is still unaware that we have a spec to be generated by codegen. To inform React Native about our spec folder, let’s add a required codegen configuration:

{
  "name": "TurboContacts",
  "version": "1.0.0",
  "peerDependencies": {
    "react": "*",
    "react-native": "*"
  },
  "codegenConfig": {
    "name": "TurboContactsSpec",
    "type": "all",
    "jsSrcsDir": "spec"
  }
}

First of all we need to select a name. This name will be used later to generate headers for us, so we can choose whatever we want. It’s unrelated to our package name per se, but it’s a good idea to use a similar name for clarity. The next property type indicates whether our module is a module (modules), view (components), or all (a mix). Although we could safely set it to modules, for the purpose of this tutorial, let’s keep it as all. The final property is jsSrcsDir, which represents the relative path to the folder where we have placed our source of truth, (the TypeScript type) for generating a module. With these configurations in place, we are now connected 🔗 !

Let’s put our connection to the test and install TurboContacts locally as a dependency:

shell
yarn add ./turbo-contacts
Info icon

Keep in mind that React Native generates codegen bindings for you with every build. However for iOS you also need to run pod install.

shell
npx pod-install

If you check closely the output of the shell, you might notice that while there are [Codegen] logs, there’s nothing directly related to the turbo-contacts package.

Framework build type is static library
[Codegen] Generating ./build/generated/ios/React-Codegen.podspec.json
Analyzing dependencies
[Codegen] Found FBReactNativeSpec
[Codegen] Found rncore
Downloading dependencies
Generating Pods project
Setting REACT_NATIVE build settings

What’s going on here?

Why does React Native still not recognize our spec folder that we connected in the package.json file?

Is our Spec invalid?

Actually, that’s not the issue.

To generate codegen bindings for iOS, we need to add a podspec file. You can think of it as a package.json, but for iOS. It’s used to store package metadata like source files, along with installation instruction of the dependencies like Fabric.

Navigate to the turbo-contacts directory and create a new file named TurboContacts.podspec:

turbo-contacts/
└── spec/
    ├── NativeTurboContacts.ts
    └── index.ts
 package.json
TurboContacts.podspec

Open the TurboContacts.podspec file and add the following content:

TurboContacts.podspec
# Require the json package to read package.json
require "json"

# Read package.json to get some metadata about our package
package = JSON.parse(File.read(File.join(__dir__, "./package.json")))

# Define the configuration of the package
Pod::Spec.new do |s|
  # Name and version are taken directly from the package.json
  s.name            = package["name"]
  s.version         = package["version"]

  # Optionally you can add other fields in package.json like
  # description, homepage, license, authors etc.
  # to keep it simple, I added them as inline strings
  # feel free to edit them however you want!
  s.homepage        = "https://reactnativecrossroads.com"
  s.summary         = "Sample Contacts module"
  s.license         = "MIT"
  s.platforms       = { :ios => min_ios_version_supported }
  s.author          = "Jacek Pudysz"
  s.source          = { :git => package["repository"], :tag => "#{s.version}" }

  # Define the source files extension that we want to recognize
  # Soon, we'll create the ios folder with our module definition
  s.source_files    = "ios/*.{h,m,mm}"

  # This part installs all required dependencies like Fabric, React-Core, etc.
  install_modules_dependencies(s)
end

I left you some comments to explain every line of the podspec. If you want o know more, check out the specification for CocoaPods.

With this addition let’s proceed and re-run previous commands:

shell
yarn add ./turbo-contacts
npx pod-install

Let’s compare the output in the shell:

Auto-linking React Native module for target `rncontacts`: TurboContacts
Framework build type is static library
[Codegen] Generating ./build/generated/ios/React-Codegen.podspec.json
Analyzing dependencies
[Codegen] Found FBReactNativeSpec
[Codegen] Found rncore
Downloading dependencies
Installing TurboContacts (1.0.0)
Generating Pods project
Setting REACT_NATIVE build settings

Great news! Our module has been successfully recognized and linked 🥳.

However, despite this success, there’s still no codegen output! Have we overlooked something again?

It seems that we forgot to enable the new architecture in the rncontacts package.

Info icon

Intentionally, I introduced several mistakes and didn’t provide the correct solution from the start. Now you can understand the variety of different errors you may encounter.

Let’s modify our command a little bit, and move it to the root package.json so it will be easier to re-install the package.

package.json
{
  "name": "rncontacts",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "android": "react-native run-android",
    "ios": "react-native run-ios",
    "lint": "eslint .",
    "postinstall": "RCT_NEW_ARCH_ENABLED=1 npx pod-install",
    "start": "react-native start",
    "test": "jest"
  },
  // ...
}

postinstall script will automatically run every time you use the yarn command. It’s a convenient way to perform tasks or configurations after the installation of dependencies.

Give it one more try and simply run yarn:

shell
yarn

Open the console and search for the [Codegen] output:

[Codegen] >>>>> Processing TurboContactsSpec
[Codegen] Generated schema: /var/folders/n_/f_dddf1j4tb28c7_cyl185200000gn/T/TurboContactsSpecqdtxa9/schema.json
[Codegen] Generated artifacts: /Users/jpudysz/Projects/rncontacts/ios/build/generated/ios

We did it!

The codegen files have been successfully generated. Now, we’re all set to proceed with the implementation of our native module. Before we dive into that, let’s inspect the output. As shown in the shell above, you can find the generated files at the path /ios/build/generated/ios.

ios/
└── build/
    └── generated/
        ├── ios/
        ├── FBReactNativeSpec/
        ├── react/
        ├── TurboContactsSpec/
        │   ├── TurboContactsSpec.h
        │   └── TurboContactsSpec-generated.mm
        ├── FBReactNativeSpecJSI.h
        ├── FBReactNativeSpecJSI-generated.cpp
        ├── React-Codegen.podspec.json
        ├── TurboContactsSpecJSI.h
        └── TurboContactsSpecJSI-generated.cpp

You can open these files to discover a lot of C++ and Objective-C code. While you might not fully understand it you can certainly identify method names such as hasContactsPermission, getAllContacts and parameters like phoneNumber.

Github icon

You can compare your changes by referring to this commit

iOS Native implementation

Now we’re going to dive in and write some Objective-C code.

You might be wondering why we’re using Objective-C instead of Swift. While Swift is indeed possible, at the current stage of TurboModules, we have to employ a thin Objective-C layer to connect everything together. Without additional input from Facebook, we can’t entirely eliminate Objective-C.

For now, I’ll provide the entire module implementation in Objective-C. In Part 6 of this series, we’ll build a Swift module and work on reducing Objective-C code as much as possible.

Before we begin implementing any methods, our first task is to create the TurboContacts module as defined in the Codegen spec. Failing to register our turbo module from Objective-C would lead to a crash in our React Native app upon launch. Let’s explore what happens and replace the entire content of App.tsx in the project root with the following:

App.tsx
import React from 'react'
import { View, StyleSheet, Text } from 'react-native'
import { TurboContacts } from './turbo-contacts/spec'

const App = () => (
    <View style={styles.container}>
        <Text>
            {String(TurboContacts.hasContactsPermission())}
        </Text>
    </View>
)

const styles = StyleSheet.create({
    container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
    },
})

export default App

Our App component consists of a single View containing a Text. In this example, we’ll attempt to retrieve information about whether the application has permission to access the phone’s contacts.

Launch the iOS simulator using the following command:

shell
yarn ios

The result is as expected – a crash occurs because the module doesn’t exist yet. Let’s address this by creating a new folder ios and adding two new files in the turbo-contacts folder:

turbo-contacts/
├── spec/
│   ├── NativeTurboContacts.ts
│   └── index.ts
└── ios/
    ├── TurboContacts.h
    └── TurboContacts.mm
package.json
TurboContacts.podspec

Before we open the Xcode let’s re-run yarn add command to link our new files:

shell
yarn add ./turbo-contacts
Info icon

As of the time of writing this tutorial, the current version of Xcode is 14. I’ve also tested it on Xcode 15-beta.8, and it resulted in an error in the C++ file. The Facebook team has been notified, and there’s an issue reported on GitHub. If you’re using the stable version of Xcode 15, the issue might already be resolved.

Open the rncontacts/ios/rncontacts.xcworkspace file in the Xcode, and watch a quick video where I will show you the location of our TurboContacts.mm and TurboContacts.h files, as well as how to edit them.

Everything is set up, and we can start coding our first module in Objective-C.

Objective-C is a language from the C family, and each implementation file is followed by a corresponding header file. That’s why we’ve created two files: the first one, TurboContacts.h where we will define the structure of our module and import other modules as needed. The second one, TurboContacts.mm, handles the actual implementation.

Open TurboContacts.h file and add following line of code:

TurboContacts.h
#import "TurboContactsSpec.h"

You might wonder where TurboContactsSpec.h came from. Well, earlier in this process, we ran the Codegen command, and it automatically generated this header file for us.

In Objective-C, imports work a bit differently from JavaScript. Instead of specifying the full file path, we simply use the file’s name, and thanks to the preprocessor, Objective-C handles the rest effortlessly.

Now, let’s declare our module:

TurboContacts.h
#import "TurboContactsSpec.h"

@interface TurboContacts : NSObject<NativeTurboContactsSpec>

@end

Using the @interface keyword we declare the TurboContacts class. In Objective-C, a class declaration starts with @interface, followed by the class name. Here, TurboContacts is declared as a subclass of NSObject, a fundamental class in Objective-C.

“NSObject is the root class of most Objective-C class hierarchies, from which subclasses inherit a basic interface to the runtime system and the ability to behave as Objective-C objects.” ~Apple

In other words this means that every class in Objective-C should be a subclass of NSObject, inheriting essential capabilities and behaving as Objective-C objects. Additionally, by conforming to the NativeTurboContactsSpec protocol, our TurboContacts class is required to provide implementations for the methods defined in that protocol.

These methods correspond to those defined in our TypeScript Spec. This conformance ensures type safety between TypeScript and Objective-C, facilitating seamless interaction and consistency across the two languages

Info icon

Protocols in Objective-C are like interfaces in TypeScript. When a class conforms to a protocol, it must implement a specific set of methods defined by that protocol.

Next, open the implementation file and declare TurboContacts class:

TurboContacts.mm
#import "TurboContacts.h"

@implementation TurboContacts

RCT_EXPORT_MODULE()

@end

We start by importing the TurboContacts header file, which contains the class declaration.

Next, @implementation TurboContacts starts block with the actual class implementation.

Finally, the RCT_EXPORT_MODULE() macro is provided by React Native to register and expose our module to the JavaScript.

You might notice a warning next to the class name in Xcode. This warning indicates that we haven’t conformed to the protocol we promised to. To quickly resolve this, hover over the warning and click the fix button. This will generate method stubs based on the protocol, saving us the effort of typing them from scratch.

TurboContacts.mm
#import "TurboContacts.h"

@implementation TurboContacts

RCT_EXPORT_MODULE()

- (NSArray<NSDictionary *> *)getAllContacts {
    // code
}

- (NSNumber *)hasContactsPermission {
    // code
}

- (void)requestContactsPermission:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
    // code
}

- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
    // code
}

@end

Now, we’re just four methods away from completing the native implementation. We’ll replace each of these methods one by one with mocked implementations to ensure that our code compiles and runs correctly in our React Native app.

This step-by-step approach helps us tackle the implementation systematically and ensures that our app remains functional during the process.

TurboContacts.mm
- (NSArray<NSDictionary *> *)getAllContacts {
    // return empty array
    NSArray *contacts = [NSArray new];

    return contacts;
}

- (NSNumber *)hasContactsPermission {
    return @NO;
}

- (void)requestContactsPermission:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
    resolve(@NO);
}

- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
    // code
}

The last part looks mysterious and this is C++ mixed with Objective-C. I promised you that we will skip the C++ but actually you gonna write one line of code 😅.

TurboContacts.mm
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
    return std::make_shared<facebook::react::NativeTurboContactsSpecJSI>(params);
}

Consider it done. We registered the TurboContacts module with RCT_EXPORT_MODULE. Later we conformed to the NativeTurboContactsSpec protocol and added the basic implementation of every method. Let’s re-run the React Native app to check if our crash gone away.

shell
yarn ios

We can see the false which maps to @NO from Objective-C:

TurboContacts.mm
#import "TurboContacts.h"

@implementation TurboContacts

RCT_EXPORT_MODULE()

- (NSArray<NSDictionary *> *)getAllContacts {
    // return empty array
    NSArray *contacts = [NSArray new];

    return contacts;
}

- (NSNumber *)hasContactsPermission {
    return @NO;
}

- (void)requestContactsPermission:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
    resolve(@NO);
}

- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
    return std::make_shared<facebook::react::NativeTurboContactsSpecJSI>(params);
}

@end

Congratulations 🥳 on reaching this point in the tutorial! You now have a solid understanding of how to establish a connection between Objective-C and a React Native app. While you’ve made significant progress, there’s more work ahead.

Info icon

Keep in mind that because we installed TurboContacts as a local module, we’re working on copies of both TurboContacts.h and TurboContacts.mm. That’s why we need to run yarn every time we add a new file. Please copy the content of both files to the turbo-contacts/ios directory; otherwise, running npx pod-install will wipe them. This issue is not present for the other two options of installing the native module, nor for installing it from the npm registry.

Github icon

You can compare your changes by referring to this commit

If you want to check out the documentation before we jump into the actual implementation here is the reference to Contacts API.

To make the app aware of Contacts Framework we need to do two things:

Go back to the TurboContacts.h file and update it with following lines:

TurboContacts.h
#import "TurboContactsSpec.h"
#import <Contacts/Contacts.h>

@interface TurboContacts : NSObject<NativeTurboContactsSpec>
    @property CNContactStore *contactsStore;
@end

First, we import the header file for Apple’s Contacts API. Did you spot that we used ”< >” instead of quotes? In Objective-C it means that we want to import a system framework instead of local file.

Moving on, in line 5, we define a property called contactsStore for our class. During the initialization process, we will assign an instance of CNContactStore to this property. This instance allows us to query and interact with contacts in our phonebook.

Now, let’s proceed with including the Contacts.framework in our project. To assist you with this step, I’ve prepared a brief video guide:

Perfect!

With everything in place, we can start implementing methods one by one. Let’s begin with the class initializer to obtain an instance of CNContactStore.

TurboContacts.mm
#import "TurboContacts.h"

@implementation TurboContacts

RCT_EXPORT_MODULE()

- (instancetype)init {
    self = [super init];

    if (self) {
        self.contactsStore = [[CNContactStore alloc] init];
    }

    return self;
}

- (NSArray<NSDictionary *> *)getAllContacts {
    // return empty array
    NSArray *contacts = [NSArray new];

    return contacts;
}

- (NSNumber *)hasContactsPermission {
    return @NO;
}

- (void)requestContactsPermission:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
    resolve(@NO);
}

- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
    return std::make_shared<facebook::react::NativeTurboContactsSpecJSI>(params);
}

@end

Now, you might be wondering, what is this monster constructor?

Well… it’s Objective-C 😅 .

You can get used to it, init method is our constructor, and [[CNContactStore alloc] init] is creating a new instance of CNContactStore. We can now access instance of CNContactStore with self.contactsStore.

Let’s proceed to the hasContactsPermission method and add a few lines of code to check whether we’ve already granted access to the contacts:

TurboContacts.mm
- (NSNumber *)hasContactsPermission {
    return @NO;
    CNAuthorizationStatus authorizationStatus = [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts];

    return @(authorizationStatus == CNAuthorizationStatusAuthorized);
}
@end

This appears to be straightforward. We can use CNContactStore and invoke a authorizationStatusForEntityType method to retrieve the authorizationStatus. Then, we simply compare it to the CNAuthorizationStatusAuthorized enum.

Info icon

In Objective-C calling methods of the instances is done with square brackets: [classInstance classMethod] and it’s equal to classInstance.classMethod() in other languages.

One is done. Let’s move to the requestContactsPermission. Keep in mind that this is an async function. Async function should always take two arguments at the end of the argument list. It’s resolve and reject. You can call only one of this functions to indicate that promise was resolved or rejected.

TurboContacts.mm
- (void)requestContactsPermission:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
    resolve(@NO);
    [self.contactsStore requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError *error) {
       if (error) {
            return reject(@"Error", @"An Error occurred while requesting permission", error);
       }

       resolve(@(granted));
    }];
}

This may also look complex, but you may think of completionHandler as a callback in JavaScript. Here is the equivalent code in JavaScript:

example.js
const requestContactsPermission = promise => {
    this.contactsStore.requestAccessForEntityType(CNEntityTypeContacts, (granted, error) => {
        if (error) {
            return promise.reject(error)
        }

        promise.resolve(granted)
    })
}

If there is an error, we can call reject() and if not we can pass boolean value of granted. Reject enforces on us three parameters:

RTCBridgeModule.h
typedef void (^RCTPromiseRejectBlock)(NSString *code, NSString *message, NSError *error);

That’s why I returned “Error” and description with error details.

We need one more method to get the contacts from phonebook. Here is the missing implementation:

TurboContacts.mm
- (NSArray<NSDictionary *> *)getAllContacts {
    NSArray *contacts = [NSArray new];
    NSMutableArray<NSDictionary *> *contacts = [NSMutableArray array];
    CNContactFetchRequest *request = [[CNContactFetchRequest alloc] initWithKeysToFetch:@[CNContactGivenNameKey, CNContactFamilyNameKey, CNContactPhoneNumbersKey]];
    NSError *error;

    [self.contactsStore enumerateContactsWithFetchRequest:request error:&error usingBlock:^(CNContact *contact, BOOL *stop) {
        if (error) {
            return;
        }

        [contacts addObject:@{
            @"firstName": contact.givenName,
            @"lastName": contact.familyName,
            @"phoneNumber": contact.phoneNumbers.count > 0
                ? contact.phoneNumbers[0].value.stringValue
                : [NSNull null]
        }];
    }];

    return contacts;
}

Our getAllContacts methods is fetching all the contacts with CNContactFetchRequest. We’re specifying that we want to only get CNContactGivenNameKey, CNContactFamilyNameKey and CNContactPhoneNumbersKey. Later on line 23 we’re iterating over the contacts and adding them to the contacts array.

I think you already grasp a basics of Objective-C. Don’t worry, you don’t need to learn it. I’m also not a big fan of Objective-C and it can be hard to understand it. Nevertheless with the few lines of code we’re able to complete the iOS native implementation.

To test it out, we need to update App.tsx (you can replace it):

App.tsx
import React, { useState } from 'react'
import { View, StyleSheet, Text, Pressable, ScrollView } from 'react-native'
import { Contact, TurboContacts } from './turbo-contacts/spec'

const App = () => {
    const [hasPermission, setHasPermission] = useState(TurboContacts.hasContactsPermission())
    const [contacts, setContacts] = useState<Array<Contact>>([])

    return (
        <ScrollView contentContainerStyle={styles.container}>
            <Text>
                Has permission: {String(hasPermission)}
            </Text>
            {!hasPermission && (
                <Pressable
                    onPress={() => {
                        TurboContacts.requestContactsPermission()
                            .then(setHasPermission)
                    }}
                >
                    <Text>
                        Request permission
                    </Text>
                </Pressable>
            )}
            {hasPermission && (
                <Pressable onPress={() => setContacts(TurboContacts.getAllContacts())}>
                    <Text>
                        Fetch contacts
                    </Text>
                </Pressable>
            )}
            {contacts.map((contact, index) => (
                <View
                    key={index}
                    style={styles.contactTile}
                >
                    <Text>
                        {contact.firstName} {contact.lastName}
                    </Text>
                    <Text>
                        {contact.phoneNumber}
                    </Text>
                </View>
            ))}
        </ScrollView>
    )
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
    },
    contactTile: {
        flexDirection: 'row',
    },
})

export default App

I highlighted all the calls to the TurboContacts API, to make it easier to follow. I added two states, one to keep the information about the permission status, and the second one to store the contacts. On the first render, we’re checking the permission status and saving it in the hasPermission state. If the user hasn’t granted permission, we will display a button with the text “Request permission.” On click, we’re calling the requestContactsPermission method asynchronously to request contacts permission. If the user grants access to the contacts, our flag hasPermission will change to true, and we will display a second button with the text “Fetch contacts.” If the user presses this button, we will call the getAllContacts method. With the result of this method, we will save the contacts in the contacts state. Finally, using the map function, we are iterating over the contacts and displaying their firstName, lastName and phoneNumber.

Let’s test the implementation:

shell
yarn ios

Our app crashed because we didn’t add the privacy string to the Info.plist. The Contacts API requires the NSContactsUsageDescription key. Let’s quickly fix it:

I used following key and value:

Privacy - Contacts Usage Description : $(PRODUCT_NAME) wants to access your contacts to list them

or if you prefer to edit the file as source code:

<key>NSContactsUsageDescription</key>
<string>$(PRODUCT_NAME) wants to access your contacts to list them</string>

Let’s give it one more try, and remember to re-build the app:

Another success, we have a working implementation 🎉 .

We need to fix one more thing. Did you spot the warning in the console?

The requiresMainQueueSetup method is used in React Native to let the framework know if a native module needs to be initialized on the main thread or not. If a module needs to be initialized on the main thread, then requiresMainQueueSetup should return YES, otherwise it should return NO. This method is optional, but if it is not implemented, React Native will issue a warning.

If you’re developing a React Native module that doesn’t involve native UI rendering, you typically won’t need to interact with the main queue. However, React Native requires you to implement the requiresMainQueueSetup method (it’s not iOS-specific), and you should return YES only if you intend to perform native iOS UI rendering. In other words, if your module doesn’t directly manipulate the UI on the main thread, you can return NO to indicate that it doesn’t require main queue setup. This method helps React Native manage and optimize performance for modules that interact with the UI.

With that in mind we can safely return NO:

TurboContacts.mm
#import "TurboContacts.h"

@implementation TurboContacts

RCT_EXPORT_MODULE()

- (instancetype)init {
    self = [super init];

    if (self) {
        self.contactsStore = [[CNContactStore alloc] init];
    }

    return self;
}

+ (BOOL)requiresMainQueueSetup
{
  return NO;
}

- (NSArray<NSDictionary *> *)getAllContacts {
    NSMutableArray<NSDictionary *> *contacts = [NSMutableArray array];
    CNContactFetchRequest *request = [[CNContactFetchRequest alloc] initWithKeysToFetch:@[CNContactGivenNameKey, CNContactFamilyNameKey, CNContactPhoneNumbersKey]];
    NSError *error;

    [self.contactsStore enumerateContactsWithFetchRequest:request error:&error usingBlock:^(CNContact *contact, BOOL *stop) {
        if (error) {
            return;
        }
        [contacts addObject:@{
            @"firstName": contact.givenName,
            @"lastName": contact.familyName,
            @"phoneNumber": contact.phoneNumbers.count > 0
                ? contact.phoneNumbers[0].value.stringValue
                : [NSNull null]
        }];
    }];

    return contacts;
}

- (NSNumber *)hasContactsPermission {
    CNAuthorizationStatus authorizationStatus = [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts];

    return @(authorizationStatus == CNAuthorizationStatusAuthorized);
}

- (void)requestContactsPermission:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
    [self.contactsStore requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError *error) {
        if (error) {
            return reject(@"Error", @"An Error occurred while requesting permission", error);
        }
        resolve(@(granted));
    }];
}

- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params {
    return std::make_shared<facebook::react::NativeTurboContactsSpecJSI>(params);
}

@end

In this section, we’ve successfully completed the iOS part of our React Native module for handling contacts. We’ve established a connection between Objective-C and our React Native app, implementing methods to request and check contacts permissions, and fetch contacts from the phonebook. While Objective-C can be a bit challenging, I hope I’ve simplified the process with clear explanations and code snippets. Our React Native app now communicates seamlessly with the iOS Contacts API, allowing us to list and display contact information.

However, we still have some important tasks ahead, such as finishing the JS implementation and adding Android support.

Let’s start with Android 🤖 next!

Info icon

To save the changes, remember to copy the content of TurboContacts.h and TurboContacts.mm one more time.

Github icon

You can compare your changes by referring to this commit

Android Native implementation

Before we dive into Android development, let’s take a moment to learn from our mistakes during the iOS implementation. Here are the key steps for getting started:

You may be wondering if there is a something like podspec for Android. And the answer is, there is not. We simply need to update the codegen specification in the package.json.

Open the turbo-contacts/package.json and add the following lines to configure code generation for Android:

{
  "name": "TurboContacts",
  "version": "1.0.0",
  "peerDependencies": {
    "react": "*",
    "react-native": "*"
  },
  "codegenConfig": {
    "name": "TurboContactsSpec",
    "type": "all",
    "jsSrcsDir": "spec",
    "android": {
      "javaPackageName": "com.turbocontacts"
    }
  }
}

These changes are necessary to inform codegen about the package name, which it will use to generate namespace in Java files. Additionally, create the following folders and files to reflect the package namespace in the folder structure: build.gradle, TurboContactsPackage.kt and TurboContactsModule.kt

turbo-contacts/
├── ios/
│   ├── TurboContacts.h
│   └── TurboContacts.mm
├── android/
│   ├── build.gradle
│   └── src
│       └── main
│           └── java
│               └── com
│                   └── turbocontacts
│                       ├── TurboContactsPackage.kt
│                       └── TurboContactsModule.kt
└── spec/
    ├── index.ts
    └── NativeTurboContacts.ts
package.json
TurboContacts.podspec

For build.gradle paste the following content:

buildscript {
  ext.safeExtGet = {prop, fallback ->
    rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
  }
  repositories {
    google()
    gradlePluginPortal()
  }
  dependencies {
    classpath("com.android.tools.build:gradle:7.3.1")
    classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.22")
  }
}

apply plugin: 'com.android.library'
apply plugin: 'com.facebook.react'
apply plugin: 'org.jetbrains.kotlin.android'

android {
  compileSdkVersion safeExtGet('compileSdkVersion', 33)
  namespace "com.turbocontacts"
}

repositories {
  mavenCentral()
  google()
}

dependencies {
  implementation 'com.facebook.react:react-native'
}

We begin with a buildscript block where we defined a function (Groovy closure) called safeExtGet. This function checks if our root project (rncontacts) contains a specific value referred to as prop. If the value is not found, it uses a fallback value. We utilize this function on line 20 to retrieve the compileSdkVersion.

Info icon

Starting on August 31, 2023, all new apps submitted to the Google Play Store must target Android 13 (API level 33) or higher.

Additionally, we establish a repositories block, which functions as a list of places where Gradle can search for dependencies essential for building the app.

Our dependencies include the build.gradle and the Kotlin plugin. Without the Kotlin plugin, we would have had to write code in Java. Additionally, we applied several plugins that can extend our project with additional tasks and functionalities. However, it’s important to note that the details of these plugins go beyond the scope of this tutorial. In the future, I plan to create a blog post on how to create a Gradle plugin.

Within the android block, we not only set the compileSdkVersion but also define the project’s namespace. This namespace corresponds to the javaPackageName used in our Codegen configuration.

Furthermore, we include another repositories block, specifying where Gradle should search for the dependencies required for compiling the app. We then declare our dependence on the react-native library, which is crucial during the compilation process.

I know that’s a lot for the first time, but you can copy this config for other projects and simply replace the namespace value.

Before we install the dependencies, we need to enable new architecture for the rncontacts project on Android. Unlike iOS, this process is more straightforward. Navigate to project root and find the /android/gradle.properties.

Locate newArchEnabled flag on line 40, and set it to true.

gradle.properties
newArchEnabled=true

Now, let’s verify if Codegen started working for our Android package. Run the following command in the terminal:

shell
yarn add ./turbo-contacts

Hmm… 🤔 there are no additional logs about Codegen in the console.

For sure you can spot the iOS ones. But it seems that we missed something again.

Did our project at least discover the android package? We can verify it by opening the root android folder in AndroidStudio. Move to the “Android” tab and check out the files. Can you spot the TurboContacts package?

I can’t see it.

The issue here lies in the fact that Codegen operates differently on Android compared to iOS. To resolve this, we must create a Kotlin class that extends TurboReactPackage.

An empty implementation of this class will suffice. Without this step, Android won’t recognize our package, causing the issue we’re facing.

Open the TurboContactsPackage.kt and add the following lines of code:

TurboContactsPackage.kt
package com.turbocontacts

import com.facebook.react.TurboReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReactModuleInfoProvider

class TurboContactsPackage : TurboReactPackage() {
  override fun getModule(name: String?, reactContext: ReactApplicationContext): NativeModule? = null

  override fun getReactModuleInfoProvider(): ReactModuleInfoProvider? = null
}

In this step, we’ve defined a new class called TurboContactsPackage that extends TurboReactPackage. We’ve also overridden two methods from the base class. The first method is responsible for retrieving our native module implementation, and for now, we’re returning null. The second method is used to get details about our package, and again, we’re returning null. We will revisit these methods once we’ve successfully generated the Codegen bindings.

Repeat the installation in the shell, and return to Android Studio:

shell
yarn add ./turbo-contacts
Info icon

If you still can’t see the TurboContacts package, go to File -> Sync Project with Gradle Files

Now, we’re all set to trigger the Codegen. To do this, open Android Studio and go to Build -> Rebuild Project.

To check the generated bindings, expand the TurboContacts/java/com.turbocontacts folder. Once you’ve done that, search for a new file named NativeTurboContactsSpec.java. This name is constructed based on the name specified in codegenConfig in your package.json file.

You might be wondering why Codegen generated a Java class, if you’re using Kotlin.

Can you still use it? Absolutely!

Kotlin has excellent interoperability with Java. You can easily import NativeTurboContactsSpec.java in Kotlin file.

Now, let’s open the empty TurboContactsModule.kt file and start with the basic implementation:

TurboContactsModule.kt
package com.turbocontacts

import com.facebook.react.bridge.ReactApplicationContext
import com.turbocontacts.NativeTurboContactsSpec

class TurboContactsModule(reactContext: ReactApplicationContext) : NativeTurboContactsSpec(reactContext) {
    override fun getName() = NAME

    companion object {
        const val NAME = "TurboContacts"
    }
}

You may agree with me that it looks much better than Objective-C. The code syntax is quite similar to TypeScript!

Now, let’s go through the file and explain it line by line.

We start with imports, where we need to import the ReactApplicationContext class. This class is defined by React Native. We’re aiming to obtain a “reactContext” for our “TurboContactsModule” class. It’s required to hook into activity and lifecycle events, and to communicate with the JavaScript side of the application.

In line 6, we use a shorthand for the class constructor. We save the reactContext in our class instance. The next line is responsible for registering our module’s name, which should match the name from the getEnforcing function in our TypeScript file.

It’s worth mentioning that we also imported NativeTurboContactsSpec. This java class has been generated by Codegen. You can open it and inspect the output. Once again, we’ve achieved type safety between the native world (Kotlin) and TypeScript.

You may also notice that the TurboContactsModule class is highlighted with a red swizzle. This is because we haven’t yet implemented the methods from Codegen. It’s a corresponding mechanism similar to Objective-C protocols.

Hover over the class name, click on the red bulb, and then select “Implement members”:

TurboContactsModule.kt
package com.turbocontacts

import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.WritableArray
import com.turbocontacts.NativeTurboContactsSpec

class TurboContactsModule(reactContext: ReactApplicationContext) : NativeTurboContactsSpec(reactContext) {
    override fun getName() = NAME
    override fun hasContactsPermission(): Boolean {
        TODO("Not yet implemented")
    }

    override fun requestContactsPermission(promise: Promise?) {
        TODO("Not yet implemented")
    }

    override fun getAllContacts(): WritableArray {
        TODO("Not yet implemented")
    }

    companion object {
        const val NAME = "TurboContacts"
    }
}

Wow! That was easy. We’ve generated all the methods, and now we’re ready to add some basic implementation.

TurboContactsModule.kt
package com.turbocontacts

import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.WritableArray
import com.turbocontacts.NativeTurboContactsSpec

class TurboContactsModule(reactContext: ReactApplicationContext) : NativeTurboContactsSpec(reactContext) {
    override fun getName() = NAME
    override fun hasContactsPermission(): Boolean {
        return false
    }

    override fun requestContactsPermission(promise?: Promise) {
    override fun requestContactsPermission(promise: Promise) {
        promise.resolve(false)
    }

    override fun getAllContacts(): WritableArray {
        return Arguments.createArray()
    }

    companion object {
        const val NAME = "TurboContacts"
    }
}

The last step is to register our module in the second file TurboContactsPackage.kt:

TurboContactsPackage.kt
package com.turbocontacts

import com.facebook.react.TurboReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider

class TurboContactsPackage : TurboReactPackage() {
  override fun getModule(name: String?, reactContext: ReactApplicationContext): NativeModule? = null
  override fun getModule(name: String?, reactContext: ReactApplicationContext): NativeModule? {
      return if (name == TurboContactsModule.NAME) {
          TurboContactsModule(reactContext)
      } else {
          null
      }
  }

  override fun getReactModuleInfoProvider(): ReactModuleInfoProvider? = null
  override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
       mapOf(
          TurboContactsModule.NAME to ReactModuleInfo(
              TurboContactsModule.NAME,
              TurboContactsModule.NAME,
              false,
              false,
              true,
              false,
              true
          )
      )
  }
}

For the getModule method, we return an instance of TurboContactsModule with reactContext passed to the constructor if the name of the package is “TurboContacts”. The name is taken from the class instance.

When it comes to the getReactModuleInfoProvider method, we need to return information about our package that conforms to the ReactModuleInfoProvider.

The ReactModuleInfo class requires 7 arguments:

ReactModuleInfoProvider.class
public ReactModuleInfo(String name, String className, boolean canOverrideExistingModule, boolean needsEagerInit, boolean hasConstants, boolean isCxxModule, boolean isTurboModule) {
    this.mName = name;
    this.mClassName = className;
    this.mCanOverrideExistingModule = canOverrideExistingModule;
    this.mNeedsEagerInit = needsEagerInit;
    this.mHasConstants = hasConstants;
    this.mIsCxxModule = isCxxModule;
    this.mIsTurboModule = isTurboModule;
}

We need to provide name, className and multiple booleans in the following order:

Now is the time to test the app in emulator. Go to shell and run following script:

shell
yarn android

If you encounter the red error message “The development server returned response error code:500”, go back to the console and enter the following commands:

shell
adb reverse tcp:8081 tcp:8081
yarn android

Another success! 😋

We’ve learned how to communicate between Android and JavaScript. You can probably agree with me that bridging for Android is a little bit easier than for iOS.

Info icon

To save the changes, remember to copy the content of TurboContactsPackage.kt and TurboContactsModule.kt to your turbo_contacts/android folder.

Github icon

You can compare your changes by referring to this commit

The final step is to implement the native methods. We will use ContactsProvider API.

Let’s start with hasContactsPermission method:

TurboContactsModule.kt
package com.turbocontacts

import android.Manifest
import android.content.pm.PackageManager
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.WritableArray
import com.turbocontacts.NativeTurboContactsSpec

class TurboContactsModule(reactContext: ReactApplicationContext) : NativeTurboContactsSpec(reactContext) {
    override fun getName() = NAME
    override fun hasContactsPermission(): Boolean {
        val permission = reactApplicationContext.checkSelfPermission(Manifest.permission.READ_CONTACTS)

        return permission == PackageManager.PERMISSION_GRANTED
    }

    override fun requestContactsPermission(promise?: Promise) {
    override fun requestContactsPermission(promise: Promise) {
        promise.resolve(false)
    }

    override fun getAllContacts(): WritableArray {
        return Arguments.createArray()
    }

    companion object {
        const val NAME = "TurboContacts"
    }
}

With the use of reactApplicationContext and checkSelfPermission we can easily verify if we user granted a permission to access READ_CONTACTS. We must also add this permission to the AndroidManifest.xml, it’s like Info.plist for iOS.

Open android folder (under project root) and navigate to /app/src/main/AndroidManifest.xml, and list READ_CONTACTS as required permission.

AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.READ_CONTACTS" />

    <application
      android:name=".MainApplication"
      android:label="@string/app_name"
      android:icon="@mipmap/ic_launcher"
      android:roundIcon="@mipmap/ic_launcher_round"
      android:allowBackup="false"
      android:theme="@style/AppTheme">
      <activity
        android:name=".MainActivity"
        android:label="@string/app_name"
        android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
        android:launchMode="singleTask"
        android:windowSoftInputMode="adjustResize"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
      </activity>
    </application>
</manifest>

You may also encounter an error in AndroidStudio that checkSelfPermission method “requires API level 23” (Android 6.0). It’s because we didn’t specify the lowest supported Android SDK. We only add information about compileSdkVersion which targets Android 14.

Info icon

You may be wondering if it’s ok, to set minSdkVersion to 23. At the time of the writing, ~98% of the users globally have Android with version 6 or higher. It’s also worth mentioning that this version was released in 2015!

In order to get rid of this issue, navigate to the turbo-contacts/android/build.gradle file, and add defaultConfig block inside android.

buildscript {
  ext.safeExtGet = {prop, fallback ->
    rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
  }
  repositories {
    google()
    gradlePluginPortal()
  }
  dependencies {
    classpath("com.android.tools.build:gradle:7.3.1")
    classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.22")
  }
}

apply plugin: 'com.android.library'
apply plugin: 'com.facebook.react'
apply plugin: 'org.jetbrains.kotlin.android'

android {
  compileSdkVersion safeExtGet('compileSdkVersion', 33)
  defaultConfig {
    minSdkVersion safeExtGet("minSdkVersion", 23)
  }

  namespace "com.turbocontacts"
}

repositories {
  mavenCentral()
  google()
}

dependencies {
  implementation 'com.facebook.react:react-native'
}

Also, update project root’s build.gradle under android/build.gradle:

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    ext {
        buildToolsVersion = "33.0.0"
        minSdkVersion = 23
        compileSdkVersion = 33
        targetSdkVersion = 33

        // We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.
        ndkVersion = "23.1.7779620"
    }
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath("com.android.tools.build:gradle")
        classpath("com.facebook.react:react-native-gradle-plugin")
    }
}
Info icon

In order to preserve the changes copy the content of the TurboContactsModule.kt file from AndroidStudio to your project.

shell
yarn add ./turbo-contacts

To re-generate the Codegen bindings in Android Studio go to Project -> Rebuild Project.

Info icon

If you encounter the error from cxx folder, go to root and remove folder under app/.cxx

If you go back to the TurboContactsModule.kt file the error should go away.

We’re ready to implement requestContactsPermission method. It may be a little bit problematic, as we can’t use a block (callback) like in Objective-C. We must identify our request with unique number, and later receive a notification with the information about the result.

Like I mentioned at the beginning, I won’t implement advanced queues for multiple requests from JavaScript (user may spam request permission button by clicking it multiple times). We will simply store promise in the class property and when we get the notification about permission result we will resolve it.

Let’s start by defining a property to store our promise:

TurboContactsModule.kt
class TurboContactsModule(reactContext: ReactApplicationContext) : NativeTurboContactsSpec(reactContext) {
    private var promise: Promise? = null

    override fun getName() = NAME
    override fun hasContactsPermission(): Boolean {
        val permission = reactApplicationContext.checkSelfPermission(Manifest.permission.READ_CONTACTS)

        return permission == PackageManager.PERMISSION_GRANTED
    }

    override fun requestContactsPermission(promise: Promise) {
        if (this.promise != null) {
            return
        }

        val activity = reactApplicationContext.currentActivity as PermissionAwareActivity

        activity.requestPermissions(arrayOf(Manifest.permission.READ_CONTACTS), 101, this)
    }

    override fun getAllContacts(): WritableArray {
        return Arguments.createArray()
    }

    companion object {
        const val NAME = "TurboContacts"
    }
}

Now, we need to add a unique request code for our permission call. When requesting permissions using the requestPermissions() method we need to provide an integer code. This value is used to identify the request when the result is returned to the calling activity.

TurboContactsModule.kt
class TurboContactsModule(reactContext: ReactApplicationContext) : NativeTurboContactsSpec(reactContext) {
    private var promise: Promise? = null
    private val requestCode: Int = 101

    override fun getName() = NAME
    override fun hasContactsPermission(): Boolean {
        val permission = reactApplicationContext.checkSelfPermission(Manifest.permission.READ_CONTACTS)

        return permission == PackageManager.PERMISSION_GRANTED
    }

    override fun requestContactsPermission(promise: Promise) {
        if (this.promise != null) {
            return
        }

        val activity = reactApplicationContext.currentActivity as PermissionAwareActivity

        activity.requestPermissions(arrayOf(Manifest.permission.READ_CONTACTS), 101, this)
    }

    override fun getAllContacts(): WritableArray {
        return Arguments.createArray()
    }

    companion object {
        const val NAME = "TurboContacts"
    }
}

Now, with everything in place we can request a permission:

TurboContactsModule.kt
package com.turbocontacts

import android.Manifest
import android.content.pm.PackageManager
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.WritableArray
import com.facebook.react.modules.core.PermissionAwareActivity
import com.turbocontacts.NativeTurboContactsSpec

class TurboContactsModule(reactContext: ReactApplicationContext) : NativeTurboContactsSpec(reactContext) {
    private var promise: Promise? = null
    private val requestCode: Int = 101

    override fun getName() = NAME
    override fun hasContactsPermission(): Boolean {
        val permission = reactApplicationContext.checkSelfPermission(Manifest.permission.READ_CONTACTS)

        return permission == PackageManager.PERMISSION_GRANTED
    }

    override fun requestContactsPermission(promise: Promise) {
        if (this.promise != null) {
            return
        }

        val activity = reactApplicationContext.currentActivity as PermissionAwareActivity

        this.promise = promise
        activity.requestPermissions(arrayOf(Manifest.permission.READ_CONTACTS), this.requestCode, this)
    }

    override fun getAllContacts(): WritableArray {
        return Arguments.createArray()
    }

    companion object {
        const val NAME = "TurboContacts"
    }
}

In the beginning, we start by using a simple if statement to check if there is a previously saved promise. This check helps us determine if the user has already requested a permission. In the production application, we had to implement a queue with pairs consisting of [requestCode, promise]. However, for the purpose of this tutorial, we will take a simpler approach. If there is another permission request in progress, we will simply ignore the next one. This is why, in line 30, we assign a new promise to the class property.

To request permission for accessing contacts, we first need to obtain an instance of currentActivity. Additionally, we should cast it to PermissionAwareActivity.

In line 31, we invoke requestPermissions with three arguments. The first argument is an array containing the desired permissions. In our case, we are only interested in READ_CONTACTS, so this array contains a single element. The second argument is a unique request code. In our simplified approach, we use a single code and a single promise, so the request code remains constant. The last argument is a PermissionListener.

You might wonder why we pass this here. Is our class PermissionListener?

It’s not yet, but we can change that.

We need to ensure that the TurboContactsModule class is aware of our permission requests. This is why we must implement the PermissionListener interface to receive notifications about permission request responses. This way, we can later identify our requests based on the request code, which is always 101 for us.

Now, let’s return to the top of the file and add the following lines:

TurboContactsModule.kt
package com.turbocontacts

import android.Manifest
import android.content.pm.PackageManager
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.WritableArray
import com.facebook.react.modules.core.PermissionAwareActivity
import com.facebook.react.modules.core.PermissionListener
import com.turbocontacts.NativeTurboContactsSpec

class TurboContactsModule(reactContext: ReactApplicationContext) : NativeTurboContactsSpec(reactContext), PermissionListener {
    private var promise: Promise? = null
    private val requestCode: Int = 101

    override fun getName() = NAME
    override fun hasContactsPermission(): Boolean {
        val permission = reactApplicationContext.checkSelfPermission(Manifest.permission.READ_CONTACTS)

        return permission == PackageManager.PERMISSION_GRANTED
    }

    override fun requestContactsPermission(promise: Promise) {
        if (this.promise != null) {
            return
        }

        val activity = reactApplicationContext.currentActivity as PermissionAwareActivity

        this.promise = promise
        activity.requestPermissions(arrayOf(Manifest.permission.READ_CONTACTS), this.requestCode, this)
    }

    override fun getAllContacts(): WritableArray {
        return Arguments.createArray()
    }

    companion object {
        const val NAME = "TurboContacts"
    }
}

Now, to handle the responses, we need to add one more method:

TurboContactsModule.kt
package com.turbocontacts

import android.Manifest
import android.content.pm.PackageManager
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.WritableArray
import com.facebook.react.modules.core.PermissionAwareActivity
import com.facebook.react.modules.core.PermissionListener
import com.turbocontacts.NativeTurboContactsSpec

class TurboContactsModule(reactContext: ReactApplicationContext) : NativeTurboContactsSpec(reactContext), PermissionListener {
    private var promise: Promise? = null
    private val requestCode: Int = 101

    override fun getName() = NAME
    override fun hasContactsPermission(): Boolean {
        val permission = reactApplicationContext.checkSelfPermission(Manifest.permission.READ_CONTACTS)

        return permission == PackageManager.PERMISSION_GRANTED
    }

    override fun requestContactsPermission(promise: Promise) {
        if (this.promise != null) {
            return
        }

        val activity = reactApplicationContext.currentActivity as PermissionAwareActivity

        this.promise = promise
        activity.requestPermissions(arrayOf(Manifest.permission.READ_CONTACTS), this.requestCode, this)
    }

    override fun getAllContacts(): WritableArray {
        return Arguments.createArray()
    }

    companion object {
        const val NAME = "TurboContacts"
    }

   override fun onRequestPermissionsResult(p0: Int, p1: Array<out String>?, p2: IntArray?): Boolean {
        TODO("Not yet implemented")
   }
}

While working in Android Studio, you might encounter some unhelpful names like p0, p1, and so on. Let’s make them more readable:

TurboContactsModule.kt
   override fun onRequestPermissionsResult(p0: Int, p1: Array<out String>?, p2: IntArray?): Boolean {
   override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>?, results: IntArray?): Boolean {
        TODO("Not yet implemented")
   }

In the response, we’ll receive a requestCode that we can compare with our own requestCode, an array of permissions indicating which permissions were requested, and finally, an array of Integer-based results. Additionally, we need to return a boolean value to indicate whether the permission request was handled or not.

Let’s modify the onRequestPermissionsResult method to suit our needs:

TurboContactsModule.kt
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>?, results: IntArray?): Boolean {
    if (requestCode != this.requestCode || this.promise == null || results == null || results.isEmpty()) {
        return false
    }

    val readContactsPermissionResult = results[0]

    this.promise?.resolve(readContactsPermissionResult == PackageManager.PERMISSION_GRANTED)

    return true
}

Within the extended if statement, we’re checking for multiple conditions to ensure that:

If everything looks good, and indeed we have matched the requestCode, we can retrieve the first element from the array. This will be our readContactsPermissionResult.

We can then pass this result to the this.promise?.resolve function, which will bridge the communication from C++ to JavaScript. You might be wondering why I used ?. It’s for Kotlin’s safety. Without it Kotlin might raise concerns that someone “could have changed it by this time” during the access.

Finally, in line 52, we return true to indicate that we have successfully handled the permission result.

Two methods are done. Let’s proceed to the final one to retrieve an array of contacts:

TurboContactsModule.kt
override fun getAllContacts(): WritableArray {
    return Arguments.createArray()
    val contactsArray = Arguments.createArray()
    val contentResolver = reactApplicationContext.contentResolver
    val cursor = contentResolver.query(
        ContactsContract.Contacts.CONTENT_URI,
        null,
        null,
        null,
        null
    ) ?: return contactsArray
}
Info icon

Don’t forget to import ContactsContract with the following line: import android.provider.ContactsContract

Of course, that’s not all of the code, but let’s go line by line with detailed explanations.

At the beginning we need to prepare the query. A cursor is an interface that provides read-only access to the result returned from a query. We’re querying ContactsContract.Contacts.CONTENT_URI which grants access to the device’s contacts.

If the cursor doesn’t exist (equals null), it means we are unable to query the contacts, and in such a case, we should return an empty array.

One noteworthy point is the presence of several null in the query. These can be extended to include additional filtering options. The following null values are:

Since we don’t require any additional filters or a specific sorting order, I intentionally left these as null.

Now, let’s proceed to retrieve our first contact:

TurboContactsModule.kt
override fun getAllContacts(): WritableArray {
    val contactsArray = Arguments.createArray()
    val contentResolver = reactApplicationContext.contentResolver
    val cursor = contentResolver.query(
        ContactsContract.Contacts.CONTENT_URI,
        null,
        null,
        null,
        null
    ) ?: return contactsArray

    cursor.run {
        while (moveToNext()) {
            val nameColumnIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)
            val phoneNumberColumnIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)
            val contactName = if (nameColumnIndex >= 0) getString(nameColumnIndex) else ""
            val phoneNumber = if (phoneNumberColumnIndex >= 0) getString(phoneNumberColumnIndex) else null

            val nameParts = contactName.split(" ")
            val firstName = nameParts.firstOrNull() ?: ""
            val lastName = nameParts.getOrNull(1) ?: ""

            val contactMap: WritableMap = Arguments.createMap()

            contactMap.putString("firstName", firstName)
            contactMap.putString("lastName", lastName)
            contactMap.putString("phoneNumber", phoneNumber)

            contactsArray.pushMap(contactMap)
        }

        close()
    }

    return contactsArray
}

Using cursor.run we can execute our query. run is an extension function from the Kotlin standard library, used to simplify the code. Following that, we have a while loop that iterates through the contacts by moving the cursor to the next contact. For each contact, we repeat lines 50-65:

TurboContactsModule.kt
val nameColumnIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)
val phoneNumberColumnIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)
val contactName = if (nameColumnIndex >= 0) getString(nameColumnIndex) else ""
val phoneNumber = if (phoneNumberColumnIndex >= 0) getString(phoneNumberColumnIndex) else null

Firstly, we aim to obtain the column indexes for DISPLAY_NAME and NUMBER. If they equal -1, it means that they were not found, similar to JavaScript. Since there is no separate column for firstName and lastName, we will split them later to match the iOS API.

This is why we have conditional statements like if (nameColumnIndex >= 0). Kotlin has a convenient feature that allows us to inline if and else statements, similar to a ternary operator. If everything went well, we save contactName as a string and phoneNumber, which can be either a string or null.

TurboContactsModule.kt
val nameParts = contactName.split(" ")
val firstName = nameParts.firstOrNull() ?: ""
val lastName = nameParts.getOrNull(1) ?: ""

The next part is for aligning our API with iOS. We need to split contactName by a space (” ”) and examine multiple parts to extract firstName and lastName.

Finally, we can put everything into a WritableMap, which will be converted into an Object in JavaScript.

TurboContactsModule.kt
val contactMap: WritableMap = Arguments.createMap()

contactMap.putString("firstName", firstName)
contactMap.putString("lastName", lastName)
contactMap.putString("phoneNumber", phoneNumber)

contactsArray.pushMap(contactMap)

We’ve got it! 🥳

We’ve completed the Android implementation. Let’s save the changes and test it.

Info icon

Copy the content of TurboContactsModule.kt from Android Studio to your project.

Github icon

You can compare your changes by referring to this commit

Something is not right. We were able to fetch permissions and request them, but unfortunately, nothing happens when we click on the Fetch Contacts button.

Let’s debug it!

Navigate back to Android Studio, open TurboContactsModule.kt, add a breakpoint and run debugging mode:

Now, try clicking on the Fetch Contacts button once more. The app’s context should pause on line 38. You can step through the code line by line using the following button:

Hmm 🤔

It appears that our code is not entering the cursor.run block. What could be the issue?

By default, the Android emulator does not come with any contacts in the phonebook 🙈

Minimize the app, and add few contacts:

Then, return to the app and try again:

We were able to display the names, but where are the phone numbers?

Info icon

It’s not possible to use contentResolver and fetch contacts with and without phone numbers in a single query.

Android complicated our implementation, and even though it initially seemed simpler, we now find ourselves leaning towards iOS.

Let’s make a modification to the query by adding the retrieval of the contact’s ID, DISPLAY_NAME, and HAS_PHONE_NUMBER.

TurboContactsModule.kt
override fun getAllContacts(): WritableArray {
    val contactsArray = Arguments.createArray()
    val contentResolver = reactApplicationContext.contentResolver
    val cursor = contentResolver.query(
        ContactsContract.Contacts.CONTENT_URI,
        null,
        null,
        null,
        null
    ) ?: return contactsArray

    cursor.run {
        while (moveToNext()) {
            val contactIdColumnIndex = cursor.getColumnIndex(ContactsContract.Contacts._ID)
            val nameColumnIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)
            val phoneNumberColumnIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)
            val hasPhoneNumberColumnIndex = cursor.getColumnIndex(ContactsContract.Contacts.HAS_PHONE_NUMBER)

            val contactName = if (nameColumnIndex >= 0) getString(nameColumnIndex) else ""
            val hasPhoneNumber = if (hasPhoneNumberColumnIndex >= 0) getInt(hasPhoneNumberColumnIndex) else 0
            val phoneNumber = if (phoneNumberColumnIndex >= 0) getString(phoneNumberColumnIndex) else null
            val contactId = if (contactIdColumnIndex >= 0) getString(contactIdColumnIndex) else null

            val nameParts = contactName.split(" ")
            val firstName = nameParts.firstOrNull() ?: ""
            val lastName = nameParts.getOrNull(1) ?: ""

            val contactMap: WritableMap = Arguments.createMap()

            contactMap.putString("firstName", firstName)
            contactMap.putString("lastName", lastName)
            contactMap.putString("phoneNumber", phoneNumber)
            contactMap.putString("phoneNumber", getPhoneNumber(hasPhoneNumber, contactId))

            contactsArray.pushMap(contactMap)
        }

        close()
    }

    return contactsArray
}

With this modification, we will obtain the contact’s ID, DISPLAY_NAME, and HAS_PHONE_NUMBER. However, we have removed Phone.NUMBER from the query because it won’t be available with a single query.

At line 69, we call a function named getPhoneNumber to retrieve the user’s phone number or return null. Please note that this function has not been implemented yet, but we can do that now:

TurboContactsModule.kt
private fun getPhoneNumber(hasPhoneNumber: Int, contactId: String?): String? {
    if (hasPhoneNumber == 0) {
        return null
    }

    val contentResolver = reactApplicationContext.contentResolver
    val phoneCursor = contentResolver.query(
        ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
        null,
        "${ContactsContract.CommonDataKinds.Phone.CONTACT_ID} = ?",
        arrayOf(contactId),
        null
    ) ?: return null

    var phoneNumber: String? = null

    phoneCursor.use {
        if (phoneCursor.moveToFirst()) {
            val phoneNumberColumnIndex = phoneCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)

            phoneNumber =  if (phoneNumberColumnIndex >= 0) phoneCursor.getString(phoneNumberColumnIndex) else null
        }
    }

    phoneCursor.close()

    return phoneNumber
}

Our function getPhoneNumber takes two arguments: the first one contains hasPhoneNumber, and the second one contains contactId. If the user doesn’t have a phone number, it doesn’t make sense to proceed with the query, and we return null.

On the other hand, if the user has a phone number, we construct a new query to fetch ContactsContract.CommonDataKinds .Phone.CONTENT_URI, which represents the phone number. This time, we need to use selection and selectionArgs to find our contact by ID.

You can think of this process as a pseudo SQL query:

SELECT * FROM ContactsContract.CommonDataKinds.Phone.CONTENT_URI WHERE CONTACT_ID = [contactId]

In the following lines, we use a new cursor to call getColumnIndex for Phone.NUMBER, and if it’s present, we assign it to the phoneNumber variable.

I understand that this might have been challenging 🥵, and if you don’t fully understand this code, don’t worry. Learning and mastering native languages like Objective-C or Kotlin can take some time.

Let’s try to fetch our contacts one more time:

Congratulations! 🥳

Our Android implementation is now complete. It has been a long journey, but now you can proudly call yourself a true React Native developer!

Info icon

For the final time copy the content of TurboContactsModule.kt to your project. Don’t worry, this is the most tedious way to link the project. Next time, we will explore other approaches.

Github icon

You can compare your changes by referring to this commit

UI Improvements

With the completed native implementations, we can enhance our App.tsx to make it look even better. We should add some basic styles to distinguish buttons from text and incorporate a SafeAreaView to make content visible on every screen.

Info icon

You can download the image from this url. If it shows error 403 forbidden, try to copy the link to another browser window.

Firstly, add the image to the project root:

rncontacts/
├── android/
├── ios/
└── turbo-contacts/
 App.tsx
hero.jpg

Now, open App.tsx, and extend import from react-native:

App.tsx
import React, { useState } from 'react'
import { View, StyleSheet, Text, Pressable, ScrollView } from 'react-native'
import { View, StyleSheet, Text, Pressable, ScrollView, Image, SafeAreaView } from 'react-native'
import { Contact, TurboContacts } from './turbo-contacts/spec'

We’re going to use SafeAreaView to ensure that the content remains visible regardless of any notches. Additionally, we need to include Image to display the hero image.

Next, wrap everything in SafeAreaView and add Image:

App.tsx
import React, { useState } from 'react'
import { View, StyleSheet, Text, Pressable, ScrollView, Image, SafeAreaView } from 'react-native'
import { Contact, TurboContacts } from './turbo-contacts/spec'

const App = () => {
    const [hasPermission, setHasPermission] = useState(TurboContacts.hasContactsPermission())
    const [contacts, setContacts] = useState<Array<Contact>>([])

    return (
        <SafeAreaView style={styles.container}>
            <ScrollView contentContainerStyle={styles.container}>
                <Image
                    source={require('./hero.jpg')}
                    style={styles.hero}
                />
                <Text>
                    Has permission: {String(hasPermission)}
                </Text>
                {!hasPermission && (
                    <Pressable
                        onPress={() => {
                            TurboContacts.requestContactsPermission()
                                .then(setHasPermission)
                        }}
                    >
                        <Text>
                            Request permission
                        </Text>
                    </Pressable>
                )}
                {hasPermission && (
                    <Pressable onPress={() => setContacts(TurboContacts.getAllContacts())}>
                        <Text>
                            Fetch contacts
                        </Text>
                    </Pressable>
                )}
                {contacts.map((contact, index) => (
                    <View
                        key={index}
                        style={styles.contactTile}
                    >
                        <Text>
                            {contact.firstName} {contact.lastName}
                        </Text>
                        <Text>
                            {contact.phoneNumber}
                        </Text>
                    </View>
                ))}
            </ScrollView>
        </SafeAreaView>
    )
}

We also need to update the styles for the container and add the missing ones for the image:

App.tsx
const styles = StyleSheet.create({
    container: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
    },
    contactTile: {
        flexDirection: 'row',
    },
    hero: {
        height: 200,
        width: 300,
        resizeMode: 'contain'
    }
})

Now, let’s replace the text for Has permission and add a few new styles for buttons and text:

App.tsx
const App = () => {
    const [hasPermission, setHasPermission] = useState(TurboContacts.hasContactsPermission())
    const [contacts, setContacts] = useState<Array<Contact>>([])

    return (
        <SafeAreaView style={styles.container}>
            <ScrollView contentContainerStyle={styles.container}>
                <Image
                    source={require('./hero.jpg')}
                    style={styles.hero}
                />
                <Text>
                    Has permission: {String(hasPermission)}
                </Text>
                <Text style={styles.heading}>
                    Turbo Contacts
                </Text>
                {!hasPermission && (
                    <Pressable
                        style={styles.button}
                        onPress={() => {
                            TurboContacts.requestContactsPermission()
                                .then(setHasPermission)
                        }}
                    >
                        <Text style={styles.buttonText}>
                            Request permission
                        </Text>
                    </Pressable>
                )}
                {hasPermission && (
                    <Pressable
                        style={styles.button}
                        onPress={() => setContacts(TurboContacts.getAllContacts())}
                    >
                        <Text style={styles.buttonText}>
                            Fetch contacts
                        </Text>
                    </Pressable>
                )}
                {contacts.map((contact, index) => (
                    <View
                        key={index}
                        style={styles.contactTile}
                    >
                        <Text>
                            {contact.firstName} {contact.lastName}
                        </Text>
                        <Text>
                            {contact.phoneNumber}
                        </Text>
                    </View>
                ))}
            </ScrollView>
        </SafeAreaView>
    )
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
    },
    contactTile: {
        flexDirection: 'row',
    },
    hero: {
        height: 200,
        width: 300,
        resizeMode: 'contain'
    },
    heading: {
        fontWeight: 'bold',
        fontSize: 26
    },
    button: {
        backgroundColor: '#D33257',
        padding: 10,
        borderRadius: 8,
        marginTop: 20
    },
    buttonText: {
        color: 'white'
    },
})

The final step is to modify the content of the map and add a few lines of styles:

App.tsx
                {contacts.map((contact, index) => (
                    <View
                        key={index}
                        style={styles.contactTile}
                    >
                        <Text>
                            {contact.firstName} {contact.lastName}
                        </Text>
                        <Text>
                            {contact.phoneNumber}
                        </Text>
                    </View>
                ))}
                {contacts.length > 0 && (
                    <View style={styles.contactsContainer}>
                        {contacts.map((contact, index) => (
                            <View
                                key={index}
                                style={styles.contactTile}
                            >
                                <Text style={styles.name}>
                                    {index + 1}. {contact.firstName} {contact.lastName}
                                </Text>
                                <Text>
                                    {contact.phoneNumber ?? '-'}
                                </Text>
                            </View>
                        ))}
                    </View>
                )}
            </ScrollView>
        </SafeAreaView>
    )
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        alignItems: 'center',
        backgroundColor: 'white'
    },
    contactTile: {
        flexDirection: 'row',
        alignSelf: 'flex-start',
        height: 30,
        justifyContent: 'space-between'
    },
    contactsContainer:{
        width: '100%',
        borderWidth: 1,
        borderRadius: 8,
        padding: 12,
        marginTop: 50
    },
    hero: {
        height: 200,
        width: 300,
        resizeMode: 'contain'
    },
    heading: {
        fontWeight: 'bold',
        fontSize: 26
    },
    button: {
        backgroundColor: '#D33257',
        padding: 10,
        borderRadius: 8,
        marginTop: 20
    },
    buttonText: {
        color: 'white'
    },
    name: {
        flex: 1,
        fontWeight: 'bold',
    }
})

export default App

Let’s check the result and explore the final app!

Wow! That was quite a journey, and we’ve only scratched the surface of Codegen, TurboModules, and the Contacts API.

Github icon

You can compare your changes by referring to this commit

Summary

In this blog post, we delved into the fascinating world of Codegen and TurboModules. The tutorial not only covers these intriguing concepts but also walks you through the creation of a basic local module utilizing the Contacts API (iOS) and the Contacts Provider (Android).

If you want to take it further check my Twitter thread 🧵 about some app ideas regarding phonebook.

Whether you’re a seasoned developer or just getting started, I’m confident you’ve picked up something new today!

Did you enjoy the post?

Subscribe to the React Native Crossroads newsletter

Subscribe now

Do you want to support me even more?

github-mona

Sponsor me on Github

kofi

Buy me a coffee

x-twitter

Share it on Twitter

Jacek Pudysz | Copyright ©2024 | All rights reserved

Built with Astro 🚀