diff --git a/ReactAndroid/build.gradle b/ReactAndroid/build.gradle index 03c17be27a9..cb8fbbb9cba 100644 --- a/ReactAndroid/build.gradle +++ b/ReactAndroid/build.gradle @@ -264,6 +264,7 @@ android { dependencies { compile fileTree(dir: 'src/main/third-party/java/infer-annotations/', include: ['*.jar']) + compile 'javax.inject:javax.inject:1' compile 'com.android.support:appcompat-v7:23.0.1' compile 'com.android.support:recyclerview-v7:23.0.1' compile 'com.facebook.fresco:fresco:0.11.0' diff --git a/ReactAndroid/src/main/java/com/facebook/react/CompositeLazyReactPackage.java b/ReactAndroid/src/main/java/com/facebook/react/CompositeLazyReactPackage.java new file mode 100644 index 00000000000..ff051ff67ee --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/CompositeLazyReactPackage.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.ModuleSpec; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; + +public class CompositeLazyReactPackage extends LazyReactPackage { + + private final List mChildReactPackages; + + /** + * The order in which packages are passed matters. It may happen that a NativeModule or + * or a ViewManager exists in two or more ReactPackages. In that case the latter will win + * i.e. the latter will overwrite the former. This re-occurrence is detected by + * comparing a name of a module. + */ + public CompositeLazyReactPackage(LazyReactPackage... args) { + mChildReactPackages = Arrays.asList(args); + } + + /** + * {@inheritDoc} + */ + @Override + public List getNativeModules(ReactApplicationContext reactContext) { + // TODO: Consider using proper name here instead of class + // This would require us to use ModuleHolder here + final Map, ModuleSpec> moduleMap = new HashMap<>(); + for (LazyReactPackage reactPackage: mChildReactPackages) { + for (ModuleSpec module: reactPackage.getNativeModules(reactContext)) { + moduleMap.put(module.getType(), module); + } + } + return new ArrayList<>(moduleMap.values()); + } + + /** + * {@inheritDoc} + */ + @Override + public List> createJSModules() { + final Set> moduleSet = new HashSet<>(); + for (ReactPackage reactPackage: mChildReactPackages) { + for (Class jsModule: reactPackage.createJSModules()) { + moduleSet.add(jsModule); + } + } + return new ArrayList(moduleSet); + } + + /** + * {@inheritDoc} + */ + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + final Map viewManagerMap = new HashMap<>(); + for (ReactPackage reactPackage: mChildReactPackages) { + for (ViewManager viewManager: reactPackage.createViewManagers(reactContext)) { + viewManagerMap.put(viewManager.getName(), viewManager); + } + } + return new ArrayList(viewManagerMap.values()); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/LazyReactPackage.java b/ReactAndroid/src/main/java/com/facebook/react/LazyReactPackage.java new file mode 100644 index 00000000000..87e5b0c4570 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/LazyReactPackage.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react; + +import java.util.ArrayList; +import java.util.List; + +import com.facebook.react.bridge.ModuleSpec; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; + +/** + * React package supporting lazy creation of native modules. + * + * TODO(t11394819): Make this default and deprecate ReactPackage + */ +public abstract class LazyReactPackage implements ReactPackage { + /** + * @param reactContext react application context that can be used to create modules + * @return list of module specs that can create the native modules + */ + public abstract List getNativeModules( + ReactApplicationContext reactContext); + + @Override + public final List createNativeModules(ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + for (ModuleSpec holder : getNativeModules(reactContext)) { + modules.add(holder.getProvider().get()); + } + return modules; + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/BUCK b/ReactAndroid/src/main/java/com/facebook/react/bridge/BUCK index 6164c32a8bc..94c83ed210c 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/BUCK @@ -19,6 +19,7 @@ android_library( exported_deps = [ react_native_dep('java/com/facebook/jni:jni'), react_native_dep('java/com/facebook/proguard/annotations:annotations'), + react_native_dep('third-party/java/jsr-330:jsr-330'), ], deps = [ react_native_dep('java/com/facebook/systrace:systrace'), diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/ModuleSpec.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/ModuleSpec.java new file mode 100644 index 00000000000..88029615427 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/ModuleSpec.java @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.bridge; + +import javax.annotation.Nullable; +import javax.inject.Provider; + +import java.lang.reflect.Constructor; + +import com.facebook.react.common.build.ReactBuildConfig; + +/** + * A specification for a native module. This exists so that we don't have to pay the cost + * for creation until/if the module is used. + * + * If your module either has a default constructor or one taking ReactApplicationContext you can use + * {@link #simple(Class)} or {@link #simple(Class, ReactApplicationContext)}} methods. + */ +public class ModuleSpec { + private static final Class[] EMPTY_SIGNATURE = {}; + private static final Class[] CONTEXT_SIGNATURE = { ReactApplicationContext.class }; + + private final Class mType; + private final Provider mProvider; + + /** + * Simple spec for modules with a default constructor. + */ + public static ModuleSpec simple(final Class type) { + return new ModuleSpec(type, new ConstructorProvider(type, EMPTY_SIGNATURE) { + @Override + public NativeModule get() { + try { + return getConstructor(type, EMPTY_SIGNATURE).newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }); + } + + /** + * Simple spec for modules with a constructor taking ReactApplicationContext. + */ + public static ModuleSpec simple( + final Class type, + final ReactApplicationContext context) { + return new ModuleSpec(type, new ConstructorProvider(type, CONTEXT_SIGNATURE) { + @Override + public NativeModule get() { + try { + return getConstructor(type, CONTEXT_SIGNATURE).newInstance(context); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }); + } + + public ModuleSpec(Class type, Provider provider) { + mType = type; + mProvider = provider; + } + + public Class getType() { + return mType; + } + + public Provider getProvider() { + return mProvider; + } + + private static abstract class ConstructorProvider implements Provider { + protected @Nullable Constructor mConstructor; + + public ConstructorProvider(Class type, Class[] signature) { + if (ReactBuildConfig.DEBUG) { + try { + mConstructor = getConstructor(type, signature); + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException("No such constructor", e); + } + } + } + + protected Constructor getConstructor( + Class mType, + Class[] signature) throws NoSuchMethodException { + if (mConstructor != null) { + return mConstructor; + } else { + return mType.getConstructor(signature); + } + } + } +} diff --git a/ReactAndroid/src/test/java/com/facebook/react/bridge/ModuleSpecTest.java b/ReactAndroid/src/test/java/com/facebook/react/bridge/ModuleSpecTest.java new file mode 100644 index 00000000000..a96a0b18da7 --- /dev/null +++ b/ReactAndroid/src/test/java/com/facebook/react/bridge/ModuleSpecTest.java @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.bridge; + +import org.junit.Rule; +import org.junit.runner.RunWith; +import org.junit.Test; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.core.classloader.annotations.SuppressStaticInitializationFor; +import org.powermock.modules.junit4.rule.PowerMockRule; +import org.powermock.reflect.Whitebox; +import org.robolectric.RobolectricTestRunner; + +import com.facebook.react.bridge.annotations.ReactModule; +import com.facebook.react.common.build.ReactBuildConfig; + +import static org.fest.assertions.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*"}) +@SuppressStaticInitializationFor("com.facebook.react.common.build.ReactBuildConfig") +@PrepareForTest({ReactBuildConfig.class}) +@RunWith(RobolectricTestRunner.class) +public class ModuleSpecTest { + @Rule + public PowerMockRule rule = new PowerMockRule(); + + @Test(expected = IllegalArgumentException.class) + public void testSimpleFailFast() { + Whitebox.setInternalState(ReactBuildConfig.class, "DEBUG", true); + ModuleSpec.simple(ComplexModule.class, mock(ReactApplicationContext.class)); + } + + @Test(expected = IllegalArgumentException.class) + public void testSimpleFailFastDefault() { + Whitebox.setInternalState(ReactBuildConfig.class, "DEBUG", true); + ModuleSpec.simple(ComplexModule.class); + } + + @Test + public void testSimpleNoFailFastRelease() { + Whitebox.setInternalState(ReactBuildConfig.class, "DEBUG", false); + ModuleSpec.simple(ComplexModule.class, mock(ReactApplicationContext.class)); + } + + @Test(expected = RuntimeException.class) + public void testSimpleFailLateRelease() { + Whitebox.setInternalState(ReactBuildConfig.class, "DEBUG", false); + ModuleSpec spec = ModuleSpec.simple(ComplexModule.class, mock(ReactApplicationContext.class)); + spec.getProvider().get(); + } + + @Test + public void testSimpleDefaultConstructor() { + Whitebox.setInternalState(ReactBuildConfig.class, "DEBUG", true); + ModuleSpec spec = ModuleSpec.simple(SimpleModule.class); + assertThat(spec.getProvider().get()).isInstanceOf(SimpleModule.class); + } + + @Test + public void testSimpleContextConstructor() { + Whitebox.setInternalState(ReactBuildConfig.class, "DEBUG", true); + ReactApplicationContext context = mock(ReactApplicationContext.class); + ModuleSpec spec = ModuleSpec.simple(SimpleContextModule.class, context); + + NativeModule module = spec.getProvider().get(); + assertThat(module).isInstanceOf(SimpleContextModule.class); + SimpleContextModule contextModule = (SimpleContextModule) module; + assertThat(contextModule.getReactApplicationContext()).isSameAs(context); + } + + @ReactModule(name = "ComplexModule") + public static class ComplexModule extends BaseJavaModule { + public ComplexModule(int a, int b) { + } + } + + @ReactModule(name = "SimpleModule") + public static class SimpleModule extends BaseJavaModule { + } + + @ReactModule(name = "SimpleContextModule") + public static class SimpleContextModule extends ReactContextBaseJavaModule { + public SimpleContextModule(ReactApplicationContext context) { + super(context); + } + } +}