The background
The typical build.gradle
files list repositories in a way similar to this:
repositories {
google()
mavenCentral()
}
This is a quick way to specify that dependencies we're using should be grabbed from one of the repositories above.
This approach has a few potential issues:
- Security, e.g: if a malicious actor placed a dependency impersonating the one we need, the script could download it and we wouldn't know.
- Slowness: the script doesn't know the exact coordinates of each dependency and would be searching in each repository starting from the top one.
A better way
A few years ago, Gradle added a functionality that allows to specify where each of the dependencies is precisely located.
These APIs include: includeModule
, includeGroup
, includeGroupByRegex
.
The good news is that we can gradually migrate to these APIs and make the dependency resolution more secure and faster straight away.
1st approach
The easiest way is to replace each repository with a set of includeGroupByRegex
entries.
Let's imagine that our project is using Retrofit and OkHttp.
Step 1: Remove mavenCentral()
from the repositories section.
Step 2: Force refresh dependencies in one of the following ways:
- sync the project,
- execute
./gradlew --refresh-dependencies
, - run the app/tests,
- remove
.m2
&.gradle/caches
folders from the machine
In Android Studio, we will see errors similar to these:
> Could not find com.squareup.okhttp3:okhttp:3.12.1.
Required by:
project :app
> Could not find com.squareup.okhttp3:okhttp-urlconnection:.
Required by:
project :app
> Could not find com.squareup.retrofit2:retrofit:2.9.0.
Step 3: Notice that all these missing files have the same group id: com.squareup
Step 4: In place of mavenCentral()
, add the below and refresh dependenices again (see step 2):
mavenCentral {
content {
includeGroupByRegex("com.squareup.*")
}
}
Step 5: Continue the same way till every dependency has been safe-listed. Do the same for google()
.
We could stop at this stage. The dependency resolution is safer and faster.
Going a bit further with includeGroup
includeGroup
is a more precise version of includeGroupByRegex
.
Let's remove includeGroupByRegex("com.squareup.*")
and inspect the errors again. We can see the group names. Let's use them and replace:
includeGroupByRegex("com.squareup.*")
with
includeGroup("com.squareup.retrofit2")
includeGroup("com.squareup.okhttp3")
Repeat that for every includeGroupByRegex
.
The dependency resolution is yet safer and faster. We could stop here.
An ultimate version - using: includeModule
Now, start removing each includeGroup
one by one and inspect the errors again. The groups and modules names shown in the error console are all we need to use includeModule()
. Let's use these and replace:
includeGroup("com.squareup.retrofit2")
with
includeModule("com.squareup.retrofit2", "retrofit")
includeModule("com.squareup.retrofit2", "converter-gson")
includeModule("com.squareup.retrofit2", "retrofit-mock")
Repeat that for every includeGroup
.
The resulting build script files will be quite large but we'll have full control over the repositories and what gets included in the app.
Conclusion
We can migrate at our own pace. We can either jump right into using includeModule
or gradually transition from general repositories to includeGroupByRegex
to includeGroup
and then finally includeModule
.
We can also use a mix of the include
s above.
As long as we replace general repositories with a set of include
s - our dependency resolution will be faster and safer. And we will know exactly what goes into our apps.
Further reading:
Top comments (0)