Fabric 原生模块:Android
现在是编写 Android 平台代码以便能够渲染 WebView 的时候了。你需要遵循的步骤包括:
- 运行 Codegen
- 编写
ReactWebView的代码 - 编写
ReactWebViewManager的代码 - 编写
ReactWebViewPackage的代码 - 在应用中注册
ReactWebViewPackage
1. 通过 Gradle 运行 Codegen
运行此命令一次以生成 IDE 可以使用的模板代码。
cd android
./gradlew generateCodegenArtifactsFromSchema
Codegen 将生成你需要实现的 ViewManager 接口以及用于 WebView 的 ViewManager 委托。
2. 编写 ReactWebView
ReactWebView 是包装 Android 原生视图的组件,React Native 在使用我们自定义组件时会渲染它。
在 android/src/main/java/com/webview 文件夹中创建一个 ReactWebView.java 或 ReactWebView.kt 文件,内容如下:
- Java
- Kotlin
package com.webview;
import android.content.Context;
import android.util.AttributeSet;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.uimanager.UIManagerHelper;
import com.facebook.react.uimanager.events.Event;
public class ReactWebView extends WebView {
public ReactWebView(Context context) {
super(context);
configureComponent();
}
public ReactWebView(Context context, AttributeSet attrs) {
super(context, attrs);
configureComponent();
}
public ReactWebView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
configureComponent();
}
private void configureComponent() {
this.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
this.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
emitOnScriptLoaded(OnScriptLoadedEventResult.success);
}
});
}
public void emitOnScriptLoaded(OnScriptLoadedEventResult result) {
ReactContext reactContext = (ReactContext) context;
int surfaceId = UIManagerHelper.getSurfaceId(reactContext);
EventDispatcher eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, getId());
WritableMap payload = Arguments.createMap();
payload.putString("result", result.name());
OnScriptLoadedEvent event = new OnScriptLoadedEvent(surfaceId, getId(), payload);
if (eventDispatcher != null) {
eventDispatcher.dispatchEvent(event);
}
}
public enum OnScriptLoadedEventResult {
success,
error
}
private class OnScriptLoadedEvent extends Event<OnScriptLoadedEvent> {
private final WritableMap payload;
OnScriptLoadedEvent(int surfaceId, int viewId, WritableMap payload) {
super(surfaceId, viewId);
this.payload = payload;
}
@Override
public String getEventName() {
return "onScriptLoaded";
}
@Override
public WritableMap getEventData() {
return payload;
}
}
}
package com.webview
import android.content.Context
import android.util.AttributeSet
import android.webkit.WebView
import android.webkit.WebViewClient
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.bridge.ReactContext
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.uimanager.events.Event
class ReactWebView: WebView {
constructor(context: Context) : super(context) {
configureComponent()
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
configureComponent()
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
configureComponent()
}
private fun configureComponent() {
this.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
this.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView, url: String) {
emitOnScriptLoaded(OnScriptLoadedEventResult.success)
}
}
}
fun emitOnScriptLoaded(result: OnScriptLoadedEventResult) {
val reactContext = context as ReactContext
val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, id)
val payload =
Arguments.createMap().apply {
putString("result", result.name)
}
val event = OnScriptLoadedEvent(surfaceId, id, payload)
eventDispatcher?.dispatchEvent(event)
}
enum class OnScriptLoadedEventResult {
success,
error;
}
inner class OnScriptLoadedEvent(
surfaceId: Int,
viewId: Int,
private val payload: WritableMap
) : Event<OnScriptLoadedEvent>(surfaceId, viewId) {
override fun getEventName() = "onScriptLoaded"
override fun getEventData() = payload
}
}
ReactWebView 继承了 Android 的 WebView,所以你可以轻松复用平台已经定义的所有属性。
该类定义了三个 Android 构造函数,但将实际初始化逻辑委托给了私有的 configureComponent 函数。这个函数负责初始化组件特定的属性:在这个例子中,设置了 WebView 的布局,并定义了自定义 WebView 行为的 WebClient。代码中,ReactWebView 在页面加载完成时通过实现 WebClient 的 onPageFinished 方法发出事件。
接着代码定义了一个辅助函数来实际触发事件。触发事件时,你需要:
- 获取
ReactContext的引用; - 获取要展示视图的
surfaceId; - 获取与视图关联的
eventDispatcher引用; - 使用
WritableMap创建事件的负载(payload); - 创建需要发送给 JavaScript 的事件对象;
- 调用
eventDispatcher.dispatchEvent发送事件。
文件最后部分包含了事件数据类型的定义:
OnScriptLoadedEventResult,定义了OnScriptLoaded事件的可能结果。- 实际的
OnScriptLoadedEvent,继承自 React Native 的Event类。
3. 编写 WebViewManager
WebViewManager 是将 React Native 运行时与原生视图连接的类。
当 React 接收到应用渲染某个特定组件的指令时,React 会使用已注册的视图管理器来创建视图并传递所有所需的属性。
以下是 ReactWebViewManager 的代码。
- Java
- Kotlin
package com.webview;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewManagerDelegate;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.viewmanagers.CustomWebViewManagerInterface;
import com.facebook.react.viewmanagers.CustomWebViewManagerDelegate;
import java.util.HashMap;
import java.util.Map;
@ReactModule(name = ReactWebViewManager.REACT_CLASS)
class ReactWebViewManager extends SimpleViewManager<ReactWebView> implements CustomWebViewManagerInterface<ReactWebView> {
private final CustomWebViewManagerDelegate<ReactWebView, ReactWebViewManager> delegate =
new CustomWebViewManagerDelegate<>(this);
@Override
public ViewManagerDelegate<ReactWebView> getDelegate() {
return delegate;
}
@Override
public String getName() {
return REACT_CLASS;
}
@Override
public ReactWebView createViewInstance(ThemedReactContext context) {
return new ReactWebView(context);
}
@ReactProp(name = "sourceUrl")
@Override
public void setSourceURL(ReactWebView view, String sourceURL) {
if (sourceURL == null) {
view.emitOnScriptLoaded(ReactWebView.OnScriptLoadedEventResult.error);
return;
}
view.loadUrl(sourceURL, new HashMap<>());
}
public static final String REACT_CLASS = "CustomWebView";
@Override
public Map<String, Object> getExportedCustomBubblingEventTypeConstants() {
Map<String, Object> map = new HashMap<>();
Map<String, Object> bubblingMap = new HashMap<>();
bubblingMap.put("phasedRegistrationNames", new HashMap<String, String>() {{
put("bubbled", "onScriptLoaded");
put("captured", "onScriptLoadedCapture");
}});
map.put("onScriptLoaded", bubblingMap);
return map;
}
}
package com.webview
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewManagerDelegate;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.viewmanagers.CustomWebViewManagerInterface;
import com.facebook.react.viewmanagers.CustomWebViewManagerDelegate;
@ReactModule(name = ReactWebViewManager.REACT_CLASS)
class ReactWebViewManager(context: ReactApplicationContext) : SimpleViewManager<ReactWebView>(), CustomWebViewManagerInterface<ReactWebView> {
private val delegate: CustomWebViewManagerDelegate<ReactWebView, ReactWebViewManager> =
CustomWebViewManagerDelegate(this)
override fun getDelegate(): ViewManagerDelegate<ReactWebView> = delegate
override fun getName(): String = REACT_CLASS
override fun createViewInstance(context: ThemedReactContext): ReactWebView = ReactWebView(context)
@ReactProp(name = "sourceUrl")
override fun setSourceURL(view: ReactWebView, sourceURL: String?) {
if (sourceURL == null) {
view.emitOnScriptLoaded(ReactWebView.OnScriptLoadedEventResult.error)
return;
}
view.loadUrl(sourceURL, emptyMap())
}
companion object {
const val REACT_CLASS = "CustomWebView"
}
override fun getExportedCustomBubblingEventTypeConstants(): Map<String, Any> =
mapOf(
"onScriptLoaded" to
mapOf(
"phasedRegistrationNames" to
mapOf(
"bubbled" to "onScriptLoaded",
"captured" to "onScriptLoadedCapture"
)))
}
ReactWebViewManager 继承了 React 的 SimpleViewManager 类并实现了由 Codegen 生成的 CustomWebViewManagerInterface。
它持有一个 CustomWebViewManagerDelegate 的引用,这也是 Codegen 生成的一个元素。
随后,它重写了 getName 方法,该方法必须返回在 Spec 的 codegenNativeComponent 函数调用时使用的同名字符串。
createViewInstance 函数负责实例化一个新的 ReactWebView。
接着,ViewManager 需要定义 React 组件的所有 props 如何更新原生视图。在示例中,你需要决定如何处理 React 传递给 WebView 的 sourceURL 属性。
最后,如果组件能发出事件,则需要通过重写 getExportedCustomBubblingEventTypeConstants 映射事件名称(用于冒泡事件),或者 getExportedCustomDirectEventTypeConstants(用于直接事件)。
4. 编写 ReactWebViewPackage
和 Native Modules 一样,Native Components 也需要实现 ReactPackage 类。这个对象用于注册组件到 React Native 运行时。
以下是 ReactWebViewPackage 的代码:
- Java
- Kotlin
package com.webview;
import com.facebook.react.BaseReactPackage;
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;
import com.facebook.react.uimanager.ViewManager;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ReactWebViewPackage extends BaseReactPackage {
@Override
public List<ViewManager<?, ?>> createViewManagers(ReactApplicationContext reactContext) {
return Collections.singletonList(new ReactWebViewManager(reactContext));
}
@Override
public NativeModule getModule(String s, ReactApplicationContext reactApplicationContext) {
if (ReactWebViewManager.REACT_CLASS.equals(s)) {
return new ReactWebViewManager(reactApplicationContext);
}
return null;
}
@Override
public ReactModuleInfoProvider getReactModuleInfoProvider() {
return new ReactModuleInfoProvider() {
@Override
public Map<String, ReactModuleInfo> getReactModuleInfos() {
Map<String, ReactModuleInfo> map = new HashMap<>();
map.put(ReactWebViewManager.REACT_CLASS, new ReactModuleInfo(
ReactWebViewManager.REACT_CLASS, // name
ReactWebViewManager.REACT_CLASS, // className
false, // canOverrideExistingModule
false, // needsEagerInit
false, // isCxxModule
true // isTurboModule
));
return map;
}
};
}
}
package com.webview
import com.facebook.react.BaseReactPackage
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
import com.facebook.react.uimanager.ViewManager
class ReactWebViewPackage : BaseReactPackage() {
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
return listOf(ReactWebViewManager(reactContext))
}
override fun getModule(s: String, reactApplicationContext: ReactApplicationContext): NativeModule? {
when (s) {
ReactWebViewManager.REACT_CLASS -> ReactWebViewManager(reactApplicationContext)
}
return null
}
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider {
mapOf(ReactWebViewManager.REACT_CLASS to ReactModuleInfo(
name = ReactWebViewManager.REACT_CLASS,
className = ReactWebViewManager.REACT_CLASS,
canOverrideExistingModule = false,
needsEagerInit = false,
isCxxModule = false,
isTurboModule = true,
)
)
}
}
ReactWebViewPackage 继承了 BaseReactPackage,并实现了注册组件到运行时所需的所有方法。
createViewManagers方法是创建管理自定义视图的 ViewManager 的工厂方法。getModule方法会根据 React Native 需要渲染的视图返回对应的 ViewManager。getReactModuleInfoProvider会提供注册模块到运行时所需的所有信息。
5. 在应用中注册 ReactWebViewPackage
最后,你需要在应用中注册 ReactWebViewPackage。通过修改 MainApplication 文件,将 ReactWebViewPackage 添加到 getPackages 方法返回的包列表中。
package com.demo
import android.app.Application
import com.facebook.react.PackageList
import com.facebook.react.ReactApplication
import com.facebook.react.ReactHost
import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.soloader.OpenSourceMergedSoMapping
import com.facebook.soloader.SoLoader
import com.webview.ReactWebViewPackage
class MainApplication : Application(), ReactApplication {
override val reactNativeHost: ReactNativeHost =
object : DefaultReactNativeHost(this) {
override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply {
add(ReactWebViewPackage())
}
override fun getJSMainModuleName(): String = "index"
override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG
override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED
override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED
}
override val reactHost: ReactHost
get() = getDefaultReactHost(applicationContext, reactNativeHost)
override fun onCreate() {
super.onCreate()
SoLoader.init(this, OpenSourceMergedSoMapping)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
load()
}
}
}