Skip to content

Commit f1105bc

Browse files
Salakarthymikee
authored andcommitted
feat: autolinking for Android with Gradle (#258)
Provides the Android/Gradle functionality to go with #256 & #254 ## CLI Changes - Updated `react-native config` to include the android `sourceDir` for react native module packages ---- ## Usage These are the one-off changes required to enable this in the React Native init template (or for existing user projects). ### Modify `android/settings.gradle` Include the following line in your Android projects `settings.gradle` file: ```groovy apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) ``` ### Modify `android/app/build.gradle` Add the following line at the bottom of your Android projects `android/app/build.gradle` file: ```groovy apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) ``` ### Modify `MainApplication.java` Import the PackageList class; ```java import com.facebook.react.PackageList; ``` Replace the `protected List<ReactPackage> getPackages()` method with the following: ```java @OverRide protected List<ReactPackage> getPackages() { @SuppressWarnings("UnnecessaryLocalVariable") List<ReactPackage> packages = new PackageList(this).getPackages(); // additional non auto detected packages can still be added here: // packages.add(new SomeReactNativePackage()); return packages; } ``` ### Testing I've tested the following scenarios locally: - User has no React Native global CLI installed - Will warn but continue without issue - User has no React Native packages installed yet - Continues as normal with no warning - User has more than one React Native package installed - Correctly adds multiple packages without issue - User has a package installed that does not support autodetection (config == null) - Prints a warning but continues gracefully - User has custom/non-linked React Native packages - Can be sucessfully manually added via `packages.add(new SomeReactNativePackage());` as documented above in `MainApplication.java` To test this in a new `react-native init` project locally: - Clone the CLI project - Run `yarn` in the CLI project - Run `cd packages/cli && yarn && yarn link` in the CLI project - (a little hacky as it's a mono-repo) - Run `yarn link "@react-native-community/cli"` on your React Native project - Make the required changes as above to your React Native project ---- ### Codegen Output Example An example of the generated `PackageList.java` class: ```java package com.facebook.react; import android.app.Application; import android.content.Context; import android.content.res.Resources; import com.facebook.react.ReactPackage; import com.facebook.react.shell.MainReactPackage; import java.util.Arrays; import java.util.List; import com.test.BuildConfig; // react-native-webview import com.reactnativecommunity.webview.RNCWebViewPackage; public class PackageList { private ReactNativeHost reactNativeHost; public PackageList(ReactNativeHost reactNativeHost) { this.reactNativeHost = reactNativeHost; } private ReactNativeHost getReactNativeHost() { return this.reactNativeHost; } private Resources getResources() { return this.getApplication().getResources(); } private Application getApplication() { return this.reactNativeHost.getApplication(); } private Context getApplicationContext() { return this.getApplication().getApplicationContext(); } public List<ReactPackage> getPackages() { return Arrays.<ReactPackage>asList( new MainReactPackage(), new RNCWebViewPackage() ); } } ```
1 parent 6c575fd commit f1105bc

File tree

3 files changed

+252
-1
lines changed

3 files changed

+252
-1
lines changed

packages/cli/src/commands/init/__fixtures__/editTemplate/node_modules/PlaceholderName

Whitespace-only changes.
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
import groovy.json.JsonSlurper
2+
import org.gradle.initialization.DefaultSettings
3+
4+
def generatedFileName = "PackageList.java"
5+
def generatedFileContentsTemplate = """
6+
package com.facebook.react;
7+
8+
import android.app.Application;
9+
import android.content.Context;
10+
import android.content.res.Resources;
11+
12+
import com.facebook.react.ReactPackage;
13+
import com.facebook.react.shell.MainReactPackage;
14+
import java.util.Arrays;
15+
import java.util.List;
16+
17+
{{ packageImports }}
18+
19+
public class PackageList {
20+
private ReactNativeHost reactNativeHost;
21+
public PackageList(ReactNativeHost reactNativeHost) {
22+
this.reactNativeHost = reactNativeHost;
23+
}
24+
25+
private ReactNativeHost getReactNativeHost() {
26+
return this.reactNativeHost;
27+
}
28+
29+
private Resources getResources() {
30+
return this.getApplication().getResources();
31+
}
32+
33+
private Application getApplication() {
34+
return this.reactNativeHost.getApplication();
35+
}
36+
37+
private Context getApplicationContext() {
38+
return this.getApplication().getApplicationContext();
39+
}
40+
41+
public List<ReactPackage> getPackages() {
42+
return Arrays.<ReactPackage>asList(
43+
new MainReactPackage(){{ packageClassInstances }}
44+
);
45+
}
46+
}
47+
"""
48+
49+
class ReactNativeModules {
50+
private Logger logger
51+
private Project project
52+
private DefaultSettings defaultSettings
53+
private ExtraPropertiesExtension extension
54+
private ArrayList<HashMap<String, String>> reactNativeModules
55+
56+
private static String LOG_PREFIX = ":ReactNative:"
57+
private static String REACT_NATIVE_CLI_BIN = "node_modules${File.separator}@react-native-community${File.separator}cli${File.separator}build${File.separator}index.js"
58+
private static String REACT_NATIVE_CONFIG_CMD = "node ${REACT_NATIVE_CLI_BIN} config"
59+
60+
ReactNativeModules(Logger logger) {
61+
this.logger = logger
62+
}
63+
64+
void applySettingsGradle(DefaultSettings defaultSettings, ExtraPropertiesExtension extraPropertiesExtension) {
65+
this.defaultSettings = defaultSettings
66+
this.extension = extraPropertiesExtension
67+
this.reactNativeModules = this.getReactNativeConfig()
68+
69+
addReactNativeModuleProjects()
70+
}
71+
72+
void applyBuildGradle(Project project, ExtraPropertiesExtension extraPropertiesExtension) {
73+
this.project = project
74+
this.extension = extraPropertiesExtension
75+
this.reactNativeModules = this.getReactNativeConfig()
76+
77+
addReactNativeModuleDependencies()
78+
}
79+
80+
/**
81+
* Include the react native modules android projects and specify their project directory
82+
*/
83+
void addReactNativeModuleProjects() {
84+
reactNativeModules.forEach { reactNativeModule ->
85+
String name = reactNativeModule["name"]
86+
String androidSourceDir = reactNativeModule["androidSourceDir"]
87+
defaultSettings.include(":${name}")
88+
defaultSettings.project(":${name}").projectDir = new File("${androidSourceDir}")
89+
}
90+
}
91+
92+
/**
93+
* Adds the react native modules as dependencies to the users `app` project
94+
*/
95+
void addReactNativeModuleDependencies() {
96+
reactNativeModules.forEach { reactNativeModule ->
97+
def name = reactNativeModule["name"]
98+
project.dependencies {
99+
// TODO(salakar): are other dependency scope methods such as `api` required?
100+
implementation project(path: ":${name}")
101+
}
102+
}
103+
}
104+
105+
/**
106+
* This returns the users project root (e.g. where the node_modules dir is located).
107+
*
108+
* This defaults to up one directory from the root android directory unless the user has defined
109+
* a `ext.reactNativeProjectRoot` extension property
110+
*
111+
* @return
112+
*/
113+
File getReactNativeProjectRoot() {
114+
if (this.extension.has("reactNativeProjectRoot")) {
115+
File rnRoot = File(this.extension.get("reactNativeProjectRoot"))
116+
// allow custom React Native project roots for non-standard directory structures
117+
this.logger.debug("${LOG_PREFIX}Using custom React Native project root path '${rnRoot.toString()}'")
118+
return rnRoot
119+
}
120+
121+
File androidRoot
122+
123+
if (this.project) {
124+
androidRoot = this.project.rootProject.projectDir
125+
} else {
126+
androidRoot = this.defaultSettings.rootProject.projectDir
127+
}
128+
129+
this.logger.debug("${LOG_PREFIX}Using default React Native project root path '${androidRoot.parentFile.toString()}'")
130+
return androidRoot.parentFile
131+
}
132+
133+
/**
134+
* Code-gen a java file with all the detected ReactNativePackage instances automatically added
135+
*
136+
* @param outputDir
137+
* @param generatedFileName
138+
* @param generatedFileContentsTemplate
139+
* @param applicationId
140+
*/
141+
void generatePackagesFile(File outputDir, String generatedFileName, String generatedFileContentsTemplate, String applicationId) {
142+
ArrayList<HashMap<String, String>>[] packages = this.reactNativeModules
143+
144+
String packageImports = ""
145+
String packageClassInstances = ""
146+
147+
if (packages.size() > 0) {
148+
packageImports = "import ${applicationId}.BuildConfig;\n\n"
149+
packageImports = packageImports + packages.collect {
150+
"// ${it.name}\n${it.packageImportPath}"
151+
}.join(';\n')
152+
packageClassInstances = ",\n " + packages.collect { it.packageInstance }.join(',')
153+
}
154+
155+
String generatedFileContents = generatedFileContentsTemplate
156+
.replace("{{ packageImports }}", packageImports)
157+
.replace("{{ packageClassInstances }}", packageClassInstances)
158+
159+
outputDir.mkdirs()
160+
final FileTreeBuilder treeBuilder = new FileTreeBuilder(outputDir)
161+
treeBuilder.file(generatedFileName).newWriter().withWriter { w ->
162+
w << generatedFileContents
163+
}
164+
}
165+
166+
/**
167+
* Runs a process to call the React Native CLI Config command and parses the output
168+
*
169+
* @return ArrayList < HashMap < String , String > >
170+
*/
171+
ArrayList<HashMap<String, String>> getReactNativeConfig() {
172+
if (this.reactNativeModules != null) return this.reactNativeModules
173+
ArrayList<HashMap<String, String>> reactNativeModules = new ArrayList<HashMap<String, String>>()
174+
175+
def cmdProcess
176+
177+
try {
178+
cmdProcess = Runtime.getRuntime().exec(REACT_NATIVE_CONFIG_CMD, null, getReactNativeProjectRoot())
179+
cmdProcess.waitFor()
180+
} catch (Exception exception) {
181+
this.logger.warn("${LOG_PREFIX}${exception.message}")
182+
this.logger.warn("${LOG_PREFIX}Automatic import of native modules failed. (UNKNOWN)")
183+
return reactNativeModules
184+
}
185+
186+
def reactNativeConfigOutput = cmdProcess.in.text
187+
def json = new JsonSlurper().parseText(reactNativeConfigOutput)
188+
def dependencies = json["dependencies"]
189+
190+
dependencies.each { name, value ->
191+
def platformsConfig = value["platforms"];
192+
def androidConfig = platformsConfig["android"]
193+
194+
if (androidConfig != null && androidConfig["sourceDir"] != null) {
195+
this.logger.info("${LOG_PREFIX}Automatically adding native module '${name}'")
196+
197+
HashMap reactNativeModuleConfig = new HashMap<String, String>()
198+
reactNativeModuleConfig.put("name", name)
199+
reactNativeModuleConfig.put("androidSourceDir", androidConfig["sourceDir"])
200+
reactNativeModuleConfig.put("packageInstance", androidConfig["packageInstance"])
201+
reactNativeModuleConfig.put("packageImportPath", androidConfig["packageImportPath"])
202+
this.logger.trace("${LOG_PREFIX}'${name}': ${reactNativeModuleConfig.toMapString()}")
203+
204+
reactNativeModules.add(reactNativeModuleConfig)
205+
} else {
206+
this.logger.info("${LOG_PREFIX}Skipping native module '${name}'")
207+
}
208+
}
209+
210+
return reactNativeModules
211+
}
212+
}
213+
214+
/** -----------------------
215+
* Exported Extensions
216+
* ------------------------ */
217+
218+
def autoModules = new ReactNativeModules(logger)
219+
220+
ext.applyNativeModulesSettingsGradle = { DefaultSettings defaultSettings ->
221+
autoModules.applySettingsGradle(defaultSettings, ext)
222+
}
223+
224+
ext.applyNativeModulesAppBuildGradle = { Project project ->
225+
autoModules.applyBuildGradle(project, ext)
226+
227+
def applicationId
228+
def generatedSrcDir = new File(buildDir, "generated/rncli/src/main/java/com/facebook/react")
229+
230+
// TODO(salakar): not sure if this is the best way of getting the package name (used to import BuildConfig)
231+
project.android.applicationVariants.all { variant ->
232+
applicationId = [variant.mergedFlavor.applicationId, variant.buildType.applicationIdSuffix].findAll().join()
233+
}
234+
235+
task generatePackageList << {
236+
autoModules.generatePackagesFile(generatedSrcDir, generatedFileName, generatedFileContentsTemplate, applicationId)
237+
}
238+
239+
preBuild.dependsOn generatePackageList
240+
241+
android {
242+
sourceSets {
243+
main {
244+
java {
245+
srcDirs += generatedSrcDir
246+
}
247+
}
248+
}
249+
}
250+
}

packages/platform-android/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"xmldoc": "^0.4.0"
1111
},
1212
"files": [
13-
"build"
13+
"build",
14+
"native_modules.gradle"
1415
]
1516
}

0 commit comments

Comments
 (0)