Skip to content

What happens when we build an apk

When you execute the flutter build apk command in the terminal, it triggers a series of steps to build a release APK for Android. Here are these steps:

1. Parsing the Command Line Arguments: The Flutter tool starts by parsing the command line arguments. This includes the build command and the apk target, along with any flags or options provided (like —debug, —release, —split-per-abi, etc.).

2. Checking Flutter & Dart SDK: The tool checks if the necessary Flutter and Dart SDK versions are installed. If not, it will prompt you to install or update them.

3. Environment Verification: The tool verifies your environment, including the presence of required Android SDK and the correct configuration of Android environment variables.

4. Project Configuration Verification: The tool checks your Flutter project configuration. If the android directory or other necessary files are missing, it will throw an error.

5. Dependency Check: The Flutter tool checks for the packages or plugins dependencies defined in your pubspec.yaml file. If new dependencies are found or if dependencies are updated, it runs flutter pub get to fetch all necessary dependencies.

6. Code Analysis: The Dart analyzer performs static code analysis to find any errors or warnings in your Dart code.

7. Compilation: The tool compiles your Dart code into native code (ARM or ARM64 for Android). The —release flag indicates that this should be a release build, which enables optimizations and disables debugging information.

The Dart code compilation into native code involves several processes:

7.1 Tree Shaking: This is the process of eliminating dead or unused code and libraries from the application. The Dart AOT (Ahead-of-Time) compiler, which is used for release builds, employs tree shaking to reduce the application size.

Tree shaking is a compiler feature that removes unused code during the compilation process. Here are the steps:

7.2 Identifying Entry Points: The compiler identifies the entry points of the application. An entry point is a part of the code that can be directly invoked by the system or user, like the main() function.

7.3 Building a Call Graph: The compiler builds a call graph, which is a representation of all the function calls in the application. This graph starts from the identified entry points.

7.4 Marking Unused Code: The compiler traverses the call graph and marks the code that is not reachable from the entry points. This is the “dead code”.

7.5 Removing Unused Code: The marked code is then removed from the final compiled application.

7.6 AOT Compilation: The Dart AOT compiler converts the Dart code into native machine code. This code can then be executed directly by the device’s CPU. This process involves parsing the Dart code, performing type checking, and then generating the machine code.

The Dart AOT compilation process involves several steps:

7.6.1 Parsing: The Dart compiler parses the source code, which involves breaking down the code into tokens and checking the syntax of the code.

7.6.2 Type Checking: The compiler checks the types of variables, function return values, and function arguments, and ensures that they are used correctly.

7.6.3 Intermediate Representation (IR) Generation: The compiler generates an intermediate representation of the code. This IR is a lower-level representation of the code which is easier for the compiler to optimize.

7.6.4 Machine Code Generation: The compiler converts the IR into machine code that can be directly executed by the CPU.

7.7 Optimization: The AOT compiler also performs optimization to make the code run faster and be smaller in size. This includes method inlining (replacing method calls with the method content), constant folding (replacing constant expressions with their results), and more.

The optimization process during AOT compilation also involves several steps:

7.7.1 Method Inlining: The compiler replaces calls to short methods with the body of the method itself. This can improve performance by reducing the overhead of method calls.

7.7.2 Constant Folding: The compiler evaluates constant expressions at compile time and replaces them with their results. For example, an expression like 2 + 3 would be replaced with 5.

7.7.3 Dead Code Elimination: The compiler removes code that doesn’t affect the program’s output. For example, if a variable is assigned a value but never used, that assignment can be removed.

7.7.4 Loop Unrolling: The compiler replaces loops with repeated code. This can improve performance by reducing the overhead of loop control structures, but it can also increase the size of the code.

7.7.5 Common Subexpression Elimination: If the same expression is computed multiple times, the compiler computes it once and reuses the result.

7.7.6 Code Layout Optimizations: The compiler rearranges the code to improve cache performance. For example, it might place frequently executed code close together to increase the likelihood that it will be in the cache.

These optimizations are applied in multiple “passes” over the code, meaning that the compiler goes through the code multiple times, applying the optimizations in each pass. The number of passes and the specific optimizations applied can vary based on the code and the compiler settings.

8 Resources and Assets: The tool bundles all resources (like images, fonts, etc.) and assets into the application package. These are defined in the pubspec.yaml file.

8.1 Asset Resolution: The tool identifies all assets listed in the pubspec.yaml file. This includes different resolutions of images (main, 2x, 3x), which are chosen based on the device’s screen density.

8.2 Asset Bundling: All the assets are then copied into an intermediate directory that will be used during the build process.

8.3 Including Assets in the APK: These assets are included in the APK such that they can be loaded at runtime by your application.

9 Building APK: The tool uses Android’s gradlew tool to build the APK. This includes Java/Kotlin compilation (if any), resource bundling, signing the APK (if a keystore is configured), and apk packaging.

9.1 Java/Kotlin Compilation: If your application includes any Java or Kotlin code (for example, if you’re using platform channels or native plugins), these are compiled into bytecode.

9.2 Resource Processing: Android resources (like XML layouts, drawables, etc.) are compiled. This also includes processing of the AndroidManifest.xml file.

9.3 DEX Generation: The Java/Kotlin bytecode is converted into DEX (Dalvik Executable) format, which can be executed by the Android runtime.

9.4 APK Packaging: The DEX files, compiled resources, and assets are packaged into an APK (Android Package). This APK includes a specific directory structure required by Android.

9.5 APK Signing: If a keystore is configured, the APK is signed with the private key. This is necessary for the Android system to verify the app’s integrity at install time. If the APK is not signed, it cannot be installed on a device.

9.6 APK Alignment: The final step is to align the APK. This is done to optimize the APK. It ensures that all uncompressed data within the APK start with a particular byte alignment, which can potentially make the app load faster on the device.

10 Splitting the APK (Optional): If the —split-per-abi flag is provided, the tool will create separate APKs for each supported ABI rather than a single, universal APK.

11 Output: The tool outputs the APK file(s) to the build/app/outputs/flutter-apk/ directory in your project. It will also print out the file path(s) in the terminal.

ChatGPT (GPT 4):
The creation of an Android APK file (Android Package Kit) is an intricate process. It involves several steps:

Compilation of Code: All source code files (Dart, Java, Kotlin) are compiled into bytecode or machine code. Dart code is compiled into native code (ARM or ARM64), and Java/Kotlin code is compiled into Java bytecode.

DEX Generation: The Java bytecode is then converted into Dalvik bytecode, which is Android’s format for bytecode. This is done by the DEX compiler (short for Dalvik Executable), which generates .dex files. If there’s no Java/Kotlin code in the Flutter project, this step will be skipped.

Processing of Resources: All XML resource files (like layouts, string resources, etc.) are compiled into a binary format. Images and other raw resources are also collected for packaging.

AAPT2: The Android Asset Packaging Tool (AAPT2) takes the compiled resources and the manifest file, and it generates an intermediate APK. This APK contains the compiled resources and the .dex files, but it is not yet final.

Code Shrinker (Optional): If you have enabled code shrinking (for example, by using ProGuard or R8), it’s at this point that unused classes and methods are removed to make the APK smaller.

APK Signing: The APK is then signed with a private key. This step ensures the integrity of the APK and verifies that it was produced by a trusted party (the developer). The signing can be done using the jarsigner tool or the apksigner tool, both provided by the Android SDK.

Zipalign: Lastly, the APK is optimized using a tool called zipalign. This tool ensures that all uncompressed data within the APK start with a particular byte alignment. This alignment optimization allows Android to use mmap() when it’s loading the APK, which is more efficient and can improve runtime performance.

APK Generation: After all these steps, the final APK is produced. It’s a single file that includes everything the Android system needs to install and run your app. The APK file is essentially a zip file that contains bytecode, resources, certificates, and manifest file.

The APK file is what you distribute to users, either through the Google Play Store or directly via side-loading. The Android system handles the installation of the APK file, extracting the contents and setting up the app on the user’s device.

You:
What happens to dart code while we generate apk

ChatGPT (GPT 4):
When building an APK from a Flutter application, the Dart code undergoes several transformations:

Parsing: The Dart compiler parses the source code, which involves reading the code and breaking it down into tokens. Any syntax errors in the Dart code will be caught at this stage.

Static Analysis: After parsing, the Dart analyzer performs static analysis on the code to find potential errors, bugs, or anti-patterns. This includes type checking, linting, and other code quality checks.

Tree Shaking: During the tree shaking process, the Dart compiler eliminates code that isn’t used or reachable. This is an essential step in optimizing the size of the final APK.

AOT Compilation: The Dart code is compiled Ahead-of-Time (AOT) into native, machine code. Unlike Just-In-Time (JIT) compilation used in debug mode, AOT compilation results in a standalone executable that can run without a Dart Virtual Machine. For Android, this is typically ARM or ARM64 machine code.

Optimization: The Dart AOT compiler applies several optimizations to make the code run faster and have a smaller footprint. This includes method inlining (replacing method calls with the method content), constant folding (replacing constant expressions with their results), and more.

Bundling: The compiled native code is bundled into the final APK along with any other necessary resources (like images, fonts, etc.) and assets.

The resulting APK, which now contains machine code instead of Dart code, can be installed on an Android device. When the user launches the app, the Android operating system loads the machine code into memory and starts executing it, beginning with the function equivalent to the main() function in the original Dart code.