📜 ⬆️ ⬇️

How to use all the features of mobile OS in React Native

There are several cross-platform solutions on the market: Cordova, Xamarin, React Native and others less well-known. Many mobile developers believe that cross-platform solutions will never allow to do what native applications can.


In the article I will debunk this myth and talk about the mechanism in React Native, which allows you to do everything that the native application is capable of. This mechanism is native modules. Under the cut - a detailed description of how to create native modules for Android and iOS.


image


Native modules in cross-platform development for mobile phones help to do several things:



Approximate scheme of the application on React Native


The operating system is running a native application. It runs at a low level runtime React Native and the code of native modules created by the application developer (or the author of the libraries for React Native). Above the level works React Native Bridge - an intermediate between native code and js. Js itself is executed inside the JS VM, whose role is played by JavaScriptCore. On iOS, it is provided by the system; on Android, the application drags it as a library.


image


We write the native module


Under Android


The plan is:



Lyrical Retreat 1 - Android Components


If your main background is Android, you can skip this digression. For developers with basic iOS or React JS experience, you need to know that an Android application may contain the following components:



In this context (khe-khe), of course, we are only interested in Application. Let me remind you that this component is the object of the application itself. You can (and for React Native applications should) implement your application class and implement this class ReactApplication interface:


package com.facebook.react; public interface ReactApplication { ReactNativeHost getReactNativeHost(); } 

It is necessary for ReactNative to know about the native packages that you want to use. To do this, our Application must return an instance of ReactNativeHost in which to list the list of packages:


 class MainApplication : Application(), ReactApplication { private val mReactNativeHost = object : ReactNativeHost(this) { override fun getPackages(): List<ReactPackage> { return Arrays.asList( MainReactPackage(), NativeLoggerPackage() ) } override fun getReactNativeHost(): ReactNativeHost { return mReactNativeHost } } 

NativeLoggerPackage is the package that we will write with you. It will only log the values ​​passed to it, and we will focus on the process of creating a native module instead of the actual functionality.


Why do I need Application to implement ReactApplication? Because inside React Native there is such a fun code:


 public class ReactActivityDelegate { protected ReactNativeHost getReactNativeHost() { return ((ReactApplication) getPlainActivity().getApplication()) .getReactNativeHost(); } } 

image


Now we are implementing the NativeLoggerPackage:


 class NativeLoggerPackage : ReactPackage { override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> { return Arrays.asList<NativeModule>(NativeLoggerModule()) } override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> { return emptyList<ViewManager<*, *>>() } } 

We will omit the createViewManagers method, it is not important to us in this article. And the createNativeModules method is important, which should return a list of created modules. Modules are classes that contain methods that can be called from js. Let's create a NativeLoggerModule:


 class NativeLoggerModule : BaseJavaModule() { override fun getName(): String { return "NativeLogger" } } 

A module must inherit at least BaseJavaModule, if you do not need access to the Android context. If there is a need for it, you must use a different base class:


 class NativeLoggerModule(context : ReactApplicationContext) : ReactContextBaseJavaModule(context) { override fun getName(): String { return "NativeLogger" } } 

In any case, you need to define the getName () method, which will return the name under which your module will be available in js, we will see it a little later.
Now let's finally create a method for js. This is done using the ReactMethod annotation:


 class NativeLoggerModule : BaseJavaModule() { override fun getName(): String { return "NativeLogger" } @ReactMethod fun logTheObject() { Log.d(name, “Method called”) } } 

Here the logTheObject method becomes available for calling from js. But it is unlikely that we just want to call methods without parameters that do not return anything. Let's deal with the arguments (java-types on the left, js on the right):


Boolean -> Bool
Integer -> Number
Double -> Number
Float -> Number
String -> String
Callback -> function
ReadableMap -> Object
ReadableArray -> Array


Suppose we want to pass a js object to the native method. ReadableMap will come to java:


 @ReactMethod fun logTheObject(map: ReadableMap) { val value = map.getString("key1") Log.d(name, "key1 = " + value) } 

In the case of an array, it will be passed to ReadableArray, the iteration of which is not a problem:


 @ReactMethod fun logTheArray(array: ReadableArray) { val size = array.size() for (index in 0 until size) { val value = array.getInt(index) Log.d(name, "array[$index] = $value") } } 

However, if you want to pass an object to the first argument, and an array to the second, then here, too, without surprises:


 @ReactMethod fun logTheMapAndArray(map: ReadableMap, array: ReadableArray): Boolean { logTheObject(map) logTheArray(array) return true } 

How is it good to call from javascript? There is nothing easier. The first step is to import NativeModules from the react-native root library:


 import { NativeModules } from 'react-native'; 

And then import our module (remember, we called it NativeLogger?):


 import { NativeModules } from 'react-native'; const nativeModule = NativeModules.NativeLogger; 

Now you can call the method:


 import { NativeModules } from 'react-native'; const nativeModule = NativeModules.NativeLogger; export const log = () => { nativeModule.logTheMapAndArray( { key1: 'value1' }, ['1', '2', '3'] ); }; 

Works! But wait, I want to know if everything is in order, whether it was possible to record what we wanted to record. What about return values?


image


But there are no return values ​​for functions from native modules. We'll have to get out, passing the function:


 @ReactMethod fun logWithCallback(map: ReadableMap, array: ReadableArray, callback: Callback) { logTheObject(map) logTheArray(array) callback.invoke("Logged") } 

Callback interface with the only method invoke (Object ... args) will come to the native code. From the side of js, it's just a function:


 import { NativeModules } from 'react-native'; const nativeModule = NativeModules.NativeLogger; export const log = () => { const result = nativeModule.logWithCallback( { key1: 'value1' }, [1, 2, 3], (message) => { console.log(`[NativeLogger] message = ${message}`) } ); }; 

Unfortunately, there are no tools in compile-time to verify the callback parameters from the native code and function in js, be careful.


Fortunately, you can use the mechanism of promises, which in the native code are supported by the Promise interface:


 @ReactMethod fun logAsync(value: String, promise: Promise) { Log.d(name, "Logging value: " + value) promise.resolve("Promise done") } 

Then you can call this code using async / await:


 import { NativeModules } from 'react-native'; const nativeModule = NativeModules.NativeLogger; export const log = async () => { const result = await nativeModule.logAsync('Logged value'); console.log(`[NativeModule] results = ${result}`); }; 

This completes the work of setting the native method in js on Android. We look at iOS.


image


Creating a native module in iOS


First of all, create the NativeLogger.h module:


 #import <Foundation/Foundation.h> #import <React/RCTBridgeModule.h> @interface NativeLogger : NSObject<RCTBridgeModule> @end 

and its implementation of NativeLogger.m:


 #import <Foundation/Foundation.h> #import "NativeLogger.h" @implementation NativeLogger { } RCT_EXPORT_MODULE(); 

RCT_EXPORT_MODULE is a macro that registers our module with ReactNative under the name of the file in which it is declared. If this name in js is not very suitable for you, you can change it:


 @implementation NativeLogger { } RCT_EXPORT_MODULE(NativeLogger); 

Now let's implement the methods that were made for Android. For this we need the parameters.


 string -> (NSString*) number -> (NSInteger*, float, double, CGFloat*, NSNumber*) boolean -> (BOOL, NSNumber*) array -> (NSArray*) object -> (NSDictionary*) function -> (RCTResponseSenderBlock) 

To declare the method, you can use the RCT_EXPORT_METHOD macro:


 RCT_EXPORT_METHOD(logTheObject:(NSDictionary*) map) { NSString *value = map[@"key1"]; NSLog(@"[NativeModule] %@", value); } 

 RCT_EXPORT_METHOD(logTheArray:(NSArray*) array) { for (id record in array) { NSLog(@"[NativeModule] %@", record); } } 

 RCT_EXPORT_METHOD(log:(NSDictionary*) map withArray:(NSArray*)array andCallback:(RCTResponseSenderBlock)block) { NSLog(@"Got the log"); NSArray* events = @[@"Logged"]; block(@[[NSNull null], events]); } 

The most interesting thing here is the support of promises. To do this, you will have to use another RCT_REMAP_METHOD macro, which takes the name of the method for js as the first argument and the method’s signature in objective-c as the second and subsequent arguments.
Instead of an interface, two arguments are passed here, RCTPromiseResolveBlock for resoliz promise and RCTPromiseRejectBlock for reject:


 RCT_REMAP_METHOD(logAsync, logAsyncWith:(NSString*)value withResolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { NSLog(@"[NativeModule] %@", value); NSArray* events = @[@"Logged"]; resolve(events); } 

That's all. The mechanism for transferring events from native modules to js will be discussed in a separate article.


Nuances



useful links



')

Source: https://habr.com/ru/post/347346/


All Articles