flutter/docs/platforms/android/How-Flutter-apps-are-compiled-with-Gradle-for-Android.md
2024-09-24 20:03:08 +00:00

152 lines
7.7 KiB
Markdown

Flutter builds APKs through a long set of steps that ultimately defer to Gradle.
The logic lives in many places: the Flutter framework, the Flutter app template,
and a mix of both Dart and Gradle scripts. This doc is a high level description
of the overall flow, which can be hard to follow given how many different
places at once it lives and how asynchronous Gradle builds are.
- The Flutter tool fetches any plugin dependencies of the Flutter app from pub
and downloads it to the pub cache. This step follows normal `pub get` behavior
and version solving.
- The Flutter tool writes project metadata to some local temporary files. This
includes things like the location of the local Flutter SDK, and paths to all
the newly downloaded plugins from pub on disk.
- The Flutter tool invokes Gradle to build the APK.
- Gradle reads the app's `build.gradle`, which applies the Flutter framework's
`flutter.gradle` plugin.
- `flutter.gradle` adds the Flutter engine and all of its support library
dependencies as a `jar` dependency to the Flutter app.
- `flutter.gradle` adds all of the Flutter plugins of an app as "subproject"
Gradle dependencies (usually).
- Gradle compiles the Flutter app and all of its subprojects (Flutter plugins)
into a root APK.
Those steps are deliberately listed as bullet points and not a numbered list.
Gradle invokes a lot of these steps at seemingly random and parallel times.
Time is relative.
## Plugins are compiled as subprojects (usually)
Flutter plugins are typically included in Flutter apps as source code and
compiled by Gradle, the same as the rest of the code in the Flutter app.
This is done by using the Gradle concept of
["subprojects"](https://docs.gradle.org/current/userguide/multi_project_builds.html#sec:subproject_configuration),
a way of structuring build tasks so that they're marked as being dependent on
each other. All Flutter apps are built with one subproject for the actual
Android code of the app and other subprojects for the Android code of any
plugins.
For example, let's assume that there's a Flutter app that uses two plugins:
`foo` and `bar`. Let's also assume `foo` depends on `baz`, so `baz` is included
as part of the build at runtime. Running `./gradlew projects` in the app's
`android` directory (after first running `flutter build apk` to give the Flutter
tool's Dart code an opportunity to set up the build) would reveal the following
project structure:
```console
$ ./gradlew projects
> Task :projects
------------------------------------------------------------
Root project
------------------------------------------------------------
Root project 'android'
+--- Project ':app'
+--- Project ':bar'
+--- Project ':baz''
\--- Project ':foo'
```
### The how
- [The Flutter tool's Dart
code](https://github.com/flutter/flutter/blob/82410a5ae7159b9f39e85b713decf532840ac70c/packages/flutter_tools/lib/src/build_system/targets/assets.dart#L115-L119)
writes a `.flutter-plugins` file containing a list of all the plugins used by
the app and their location on disk.
- [`flutter.gradle`](https://github.com/flutter/flutter/blob/508c35940b4989c400e7b5a2851d03e4bd0b2ca2/packages/flutter_tools/gradle/flutter.gradle#L243-L264)
reads from this file and actually adds the plugins as Gradle subproject
dependencies of the app.
- This also depends on the Flutter app having a block of code in its template
that actually references and builds these subprojects once they're defined,
see ["hello world" as an example of the app's Gradle
script](https://github.com/flutter/flutter/blob/508c35940b4989c400e7b5a2851d03e4bd0b2ca2/examples/hello_world/android/settings.gradle#L15-L19).
### Special Considerations
Including plugin dependencies as source code and then compiling them is not
something Gradle generally expects apps to be doing. In the past we've hit
several issues caused by Gradle tooling around dependency management being built
for typical Android patterns (including dependencies as `.aars` or `.jars`) and
breaking down for the Flutter use case.
For example:
- Plugins have their own `build.gradle` scripts that may conflict with the app
and other plugins in unexpected ways. How Gradle handles these conflicts is
situational and sometimes undefined behavior.
- If a plugin has a problem in its Android code, the entire user's app will fail
to compile with a stack trace that may or may not really point to the plugin's
code on disk.
- If the plugin's code conflicts with the app's Android source code (for
example, the plugin has a conflicting transitive dependency), Gradle will
generally fail to compile with difficult to translate stack traces.
- These subprojects are added in an `afterEvaluate` hook in `flutter.gradle`.
Generally adding any logic in `afterEvaluate` blocks is discouraged because it
runs after so much of Gradle's standard pipeline.
## Sometimes plugins are compiled as AARs
Because of the problems with compiling the plugins as subprojects, the
Flutter team explored switching the plugins to instead be compiled first as
standalone AARs and then included as binary dependencies as is more typical for
Android dependencies and expected by Gradle. This effort ended up being blocked,
but is still occasionally automatically used as a fallback attempt when one of the typical
problems with building as subprojects is detected as failing a build today.
The basic AAR flow goes like this:
- The Flutter tool fetches any plugin dependencies of the Flutter app from pub
and downloads it to the pub cache. This step follows normal `pub get` behavior
and version solving.
- The Flutter tool writes project metadata to some local temporary files. This
includes things like the location of the local Flutter SDK, and paths to all
the newly downloaded plugins from pub on disk.
- The Flutter tool invokes Gradle to build the APK.
- Gradle reads the app's `build.gradle`, which applies the Flutter framework's
`flutter.gradle` plugin.
- `flutter.gradle` adds the Flutter engine and all of its support library
dependencies as a `jar` dependency to the Flutter app.
- `flutter.gradle` compiles all of the plugins individually into `aars`, and
adds all of the Flutter plugins as `aar` dependencies to the Flutter app.
- Gradle compiles the Flutter app and all of its binary dependencies into a root
APK.
### Why this isn't the standard
There were a couple blocking issues that kept us from following this flow in all
cases:
- The implementation of this depends on `TaskInternal.execute()`, a Gradle API
that's been removed with no replacement in Gradle 5.
- This style of build is extremely slow compared to the previous build flow.
- The plugins currently in the Flutter ecosystem were written under the previous
build system and in many cases depend on the transitive dependency bleed to
work. For example, many plugins built this way will fail to compile
individually because they're missing an explicit dependency on
`androidx.annotation`. There are also other plugins that deliberately expose
their own transitive `jar` dependencies in a way where they're automatically
included in the app at build time by the subproject system, but apps including them as
AARs will fail with "Class not found errors" unless the transitive plugin dependencies
from the plugin are manually added into the apps' Gradle dependencies as well.
### When this is still used
We still fall back on this as a silent retry if we detect that the failure is
caused by one of the likely error cases from the subproject structure:
specifically, if we detect that the build has failed because of a likely
AndroidX incompatibility issue. Jetifier is one of the Gradle tools that doesn't
function normally with the subproject structure, so building the plugins as AARs
can sometimes fix errors in that class.