【React Native】文件翻譯閱讀紀錄 - 指南(Android) - 本機模組

by - 上午9:00

Facebook Open Source React Native


本機模組


有時,應用程序需要訪問React Native尚未擁有相應模塊的平台API。也許您希望重用一些現有的Java代碼,而不必在JavaScript中重新實現它,或者編寫一些高性能的多線程代碼,例如用於圖像處理,數據庫或任何數量的高級擴展。

我們設計了React Native,使您可以編寫真正的本機代碼並可以訪問平台的全部功能。這是一個更高級的功能,我們不希望它成為通常開發過程的一部分,但它必須存在。如果React Native不支持您需要的本機功能,您應該能夠自己構建它。

啟用 Gradle

如果您打算在Java代碼中進行更改,我們建議您啟用 Gradle Daemon 以加快構建速度。

Toast 模塊

本指南將使用Toast示例。假設我們希望能夠從JavaScript創建一個Toast消息。

我們首先創建一個本機模塊。本機模塊是一個Java類,它通常擴展ReactContextBaseJavaModule類並實現JavaScript所需的功能。我們的目標是能夠編寫ToastExample.show('Awesome',ToastExample.SHORT);從JavaScript在屏幕上顯示一個簡短的Toast 。
建立一個新的 Java Class ToastModule.java 在android/app/src/main/java/com/your-app-name/ 資料夾下
// ToastModule.java

package com.facebook.react.modules.toast;

import android.widget.Toast;

import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;

import java.util.Map;
import java.util.HashMap;

public class ToastModule extends ReactContextBaseJavaModule {

  private static final String DURATION_SHORT_KEY = "SHORT";
  private static final String DURATION_LONG_KEY = "LONG";

  public ToastModule(ReactApplicationContext reactContext) {
    super(reactContext);
  }
}
ReactContextBaseJavaModule 要求實現一個名為 getName 的方法。此方法的目的是返回在JavaScript中表示此類的 NativeModule 的字符串名稱。所以在這裡我們將調用此ToastExample,以便我們可以通過 JavaScript中的 React.NativeModules.ToastExample訪問它。
  @Override
  public String getName() {
    return "ToastExample";
  }
一個名為getConstants的可選方法返回公開給JavaScript的常量值。它的實現不是必需的,但對於需要從JavaScript同步傳遞到Java的關鍵預定義值非常有用。
  @Override
  public Map<String, Object> getConstants() {
    final Map<String, Object> constants = new HashMap<>();
    constants.put(DURATION_SHORT_KEY, Toast.LENGTH_SHORT);
    constants.put(DURATION_LONG_KEY, Toast.LENGTH_LONG);
    return constants;
  }
要向JavaScript公開方法,必須使用@ReactMethod註釋Java方法。橋接方法的返回類型始終無效。 React Native橋是異步的,因此將結果傳遞給JavaScript的唯一方法是使用回調或發出事件(見下文)。
  @ReactMethod
  public void show(String message, int duration) {
    Toast.makeText(getReactApplicationContext(), message, duration).show();
  }

參數類型

使用 @ReactMethod 註釋的方法支持以下參數類型,它們直接映射到它們的JavaScript等價物
Boolean -> Bool
Integer -> Number
Double -> Number
Float -> Number
String -> String
Callback -> function
ReadableMap -> Object
ReadableArray -> Array
閱讀有關 ReadableMap 和 ReadableArray

註冊模塊

Java中的最後一步是註冊模塊;這發生在您的應用包的createNativeModules中。如果模塊未註冊,則無法從JavaScript獲得。
創建一個名為CustomToastPackage.java 在android/app/src/main/java/com/your-app-name/ 資料內容如下:
// CustomToastPackage.java

package com.facebook.react.modules.toast;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CustomToastPackage implements ReactPackage {

  @Override
  public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
    return Collections.emptyList();
  }

  @Override
  public List<NativeModule> createNativeModules(
                              ReactApplicationContext reactContext) {
    List<NativeModule> modules = new ArrayList<>();

    modules.add(new ToastModule(reactContext));

    return modules;
  }

} 
需要在MainApplication.java文件的getPackages方法中提供該包。此文件存在於react-native應用程序目錄中的android文件夾下。該文件的路徑是: android/app/src/main/java/com/your-app-name/MainApplication.java.
protected List<ReactPackage> getPackages() {
    return Arrays.<ReactPackage>asList(
            new MainReactPackage(),
            new CustomToastPackage()); // <-- Add this line with your package name.
}
為了使從JavaScript訪問新功能變得更加簡單,通常將本機模塊包裝在JavaScript模塊中。這不是必需的,但每次都可以節省庫的使用者從NativeModules中取出它的需要。此JavaScript文件也成為添加任何JavaScript端功能的好位置。
/**
 * This exposes the native ToastExample module as a JS module. This has a
 * function 'show' which takes the following parameters:
 *
 * 1. String message: A string with the text to toast
 * 2. int duration: The duration of the toast. May be ToastExample.SHORT or
 *    ToastExample.LONG
 */
import {NativeModules} from 'react-native';
module.exports = NativeModules.ToastExample;
現在,您可以從其他JavaScript文件中調用此方法:
import ToastExample from './ToastExample';

ToastExample.show('Awesome', ToastExample.SHORT);

Beyond Toasts

回調

本機模塊還支持一種特殊的參數 - 回調。在大多數情況下,它用於向JavaScript提供函數調用結果。
import com.facebook.react.bridge.Callback;

public class UIManagerModule extends ReactContextBaseJavaModule {

...

  @ReactMethod
  public void measureLayout(
      int tag,
      int ancestorTag,
      Callback errorCallback,
      Callback successCallback) {
    try {
      measureLayout(tag, ancestorTag, mMeasureBuffer);
      float relativeX = PixelUtil.toDIPFromPixel(mMeasureBuffer[0]);
      float relativeY = PixelUtil.toDIPFromPixel(mMeasureBuffer[1]);
      float width = PixelUtil.toDIPFromPixel(mMeasureBuffer[2]);
      float height = PixelUtil.toDIPFromPixel(mMeasureBuffer[3]);
      successCallback.invoke(relativeX, relativeY, width, height);
    } catch (IllegalViewOperationException e) {
      errorCallback.invoke(e.getMessage());
    }
  }

...
可以使用以下方法在JavaScript中訪問此方法:
UIManager.measureLayout(
  100,
  100,
  (msg) => {
    console.log(msg);
  },
  (x, y, width, height) => {
    console.log(x + ':' + y + ':' + width + ':' + height);
  }
);
本機模塊應該僅調用一次回調。但是,它可以存儲回調並在以後調用它。

非常重要的是要強調在本機函數完成後不立即調用回調 - 請記住橋接通信是異步的,並且這也與運行循環相關聯。

Promises(承諾)

本機模塊也可以履行承諾,這可以簡化您的代碼,尤其是在使用ES2016的async / await語法時。當橋接本機方法的最後一個參數是Promise時,其相應的JS方法將返回一個JS Promise對象。

重構上面的代碼以使用promise而不是回調看起來像這樣:
import com.facebook.react.bridge.Promise;

public class UIManagerModule extends ReactContextBaseJavaModule {

...
  private static final String E_LAYOUT_ERROR = "E_LAYOUT_ERROR";
  @ReactMethod
  public void measureLayout(
      int tag,
      int ancestorTag,
      Promise promise) {
    try {
      measureLayout(tag, ancestorTag, mMeasureBuffer);

      WritableMap map = Arguments.createMap();

      map.putDouble("relativeX", PixelUtil.toDIPFromPixel(mMeasureBuffer[0]));
      map.putDouble("relativeY", PixelUtil.toDIPFromPixel(mMeasureBuffer[1]));
      map.putDouble("width", PixelUtil.toDIPFromPixel(mMeasureBuffer[2]));
      map.putDouble("height", PixelUtil.toDIPFromPixel(mMeasureBuffer[3]));

      promise.resolve(map);
    } catch (IllegalViewOperationException e) {
      promise.reject(E_LAYOUT_ERROR, e);
    }
  }

...
此方法的JavaScript副本返回Promise。這意味著您可以在異步函數中使用await關鍵字來調用它並等待其結果:
async function measureLayout() {
  try {
    var {relativeX, relativeY, width, height} = await UIManager.measureLayout(
      100,
      100
    );

    console.log(relativeX + ':' + relativeY + ':' + width + ':' + height);
  } catch (e) {
    console.error(e);
  }
}

measureLayout();

線程

本機模塊不應該對它們被調用的線程有任何假設,因為當前的分配將來會發生變化。如果需要阻塞調用,則應將繁重的工作分派給內部管理的工作線程,並從那里分配任何回調。

將事件發送到 JavaScript

本機模塊可以將事件發送到JavaScript,而無需直接調用。最簡單的方法是使用RCTDeviceEventEmitter,它可以從ReactContext獲得,如下面的代碼片段所示。
...
private void sendEvent(ReactContext reactContext,
                       String eventName,
                       @Nullable WritableMap params) {
  reactContext
      .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
      .emit(eventName, params);
}
...
WritableMap params = Arguments.createMap();
...
sendEvent(reactContext, "keyboardWillShow", params);
然後,JavaScript模塊可以使用Subscribable mixin通過addListenerOn註冊接收事件。
import { DeviceEventEmitter } from 'react-native';
...

var ScrollResponderMixin = {
  mixins: [Subscribable.Mixin],


  componentWillMount: function() {
    ...
    this.addListenerOn(DeviceEventEmitter,
                       'keyboardWillShow',
                       this.scrollResponderKeyboardWillShow);
    ...
  },
  scrollResponderKeyboardWillShow:function(e: Event) {
    this.keyboardWillOpenTo = e;
    this.props.onKeyboardWillShow && this.props.onKeyboardWillShow(e);
  },
您還可以直接使用 DeviceEventEmitter 模塊來偵聽事件。
...
componentWillMount: function() {
  DeviceEventEmitter.addListener('keyboardWillShow', function(e: Event) {
    // handle event.
  });
}
...

從 startActivityForResult 獲取活動結果

如果要從使用startActivityForResult開始的活動中獲取結果,則需要收聽onActivityResult。為此,您必須擴展BaseActivityEventListener或實現ActivityEventListener。前者是首選,因為它對API更改更具彈性。然後,您需要在模塊的構造函數中註冊偵聽器,
reactContext.addActivityEventListener(mActivityResultListener);
現在,您可以通過實現以下方法來監聽 onActivityResult:
@Override
public void onActivityResult(
  final Activity activity,
  final int requestCode,
  final int resultCode,
  final Intent intent) {
  // Your logic here
}
我們將實現一個簡單的圖像選擇器來演示這一點。圖像選擇器會將方法pickImage暴露給JavaScript,它將在調用時返回圖像的路徑。

public class ImagePickerModule extends ReactContextBaseJavaModule {

  private static final int IMAGE_PICKER_REQUEST = 467081;
  private static final String E_ACTIVITY_DOES_NOT_EXIST = "E_ACTIVITY_DOES_NOT_EXIST";
  private static final String E_PICKER_CANCELLED = "E_PICKER_CANCELLED";
  private static final String E_FAILED_TO_SHOW_PICKER = "E_FAILED_TO_SHOW_PICKER";
  private static final String E_NO_IMAGE_DATA_FOUND = "E_NO_IMAGE_DATA_FOUND";

  private Promise mPickerPromise;

  private final ActivityEventListener mActivityEventListener = new BaseActivityEventListener() {

    @Override
    public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent intent) {
      if (requestCode == IMAGE_PICKER_REQUEST) {
        if (mPickerPromise != null) {
          if (resultCode == Activity.RESULT_CANCELED) {
            mPickerPromise.reject(E_PICKER_CANCELLED, "Image picker was cancelled");
          } else if (resultCode == Activity.RESULT_OK) {
            Uri uri = intent.getData();

            if (uri == null) {
              mPickerPromise.reject(E_NO_IMAGE_DATA_FOUND, "No image data found");
            } else {
              mPickerPromise.resolve(uri.toString());
            }
          }

          mPickerPromise = null;
        }
      }
    }
  };

  public ImagePickerModule(ReactApplicationContext reactContext) {
    super(reactContext);

    // Add the listener for `onActivityResult`
    reactContext.addActivityEventListener(mActivityEventListener);
  }

  @Override
  public String getName() {
    return "ImagePickerModule";
  }

  @ReactMethod
  public void pickImage(final Promise promise) {
    Activity currentActivity = getCurrentActivity();

    if (currentActivity == null) {
      promise.reject(E_ACTIVITY_DOES_NOT_EXIST, "Activity doesn't exist");
      return;
    }

    // Store the promise to resolve/reject when picker returns data
    mPickerPromise = promise;

    try {
      final Intent galleryIntent = new Intent(Intent.ACTION_PICK);

      galleryIntent.setType("image/*");

      final Intent chooserIntent = Intent.createChooser(galleryIntent, "Pick an image");

      currentActivity.startActivityForResult(chooserIntent, IMAGE_PICKER_REQUEST);
    } catch (Exception e) {
      mPickerPromise.reject(E_FAILED_TO_SHOW_PICKER, e);
      mPickerPromise = null;
    }
  }
}

聽LifeCycle活動

監聽活動的LifeCycle事件(如onResume,onPause等)與我們實現ActivityEventListener的方式非常相似。該模塊必須實現LifecycleEventListener。然後,您需要在模塊的構造函數中註冊一個偵聽器,
reactContext.addLifecycleEventListener(this);
現在,您可以通過實現以下方法來監聽活動的LifeCycle事件:
@Override
public void onHostResume() {
    // Activity `onResume`
}

@Override
public void onHostPause() {
    // Activity `onPause`
}

@Override
public void onHostDestroy() {
    // Activity `onDestroy`
}



You May Also Like

0 意見