How Flutter does conditional compilation
Tags: flutter, gradle, android
Published: 2021-06-17 08:06:27

When we are using `C/C++`, if there are some codes needn't be compiled, all we have to do is add some conditional compilation codes ( `#ifdef #endif`), but for `Java`, `Kotlin`, `Dart`, it is hard to achieve that - not it can't be done, but the compiler doesn't support the "conditional compilation" features and in order to achieve the same results, we need do it in other ways.

### The reason
It started when I wanted to distribute [DaRemote](https://play.google.com/store/apps/details?id=com.deskangel.daremote) to Huawei app store. After connecting to `HMS Core`, I found that there are some extra permissions. For example, `HMS Core` Sdk requires `permission to install apps`, and the app distributed to Huawei app store does not need `Google play settlement service` permission.

Therefore, it is desirable to exclude some codes and `SDKs` and related `packages` from the compilation environment according to different conditions.

And to achieve this, it needs to be done in two parts: one for Android native side and one for Flutter/Dart side.

### Java & Kotlin implementation
#### Configure build.gradle file

The native side for Android is compiled using `gradle`, so the setup of "conditional compilation" is also done through the configuration of `build.gradle` file.

Gradle supports setting different flavors to achieve different compilation configurations. Previously, I just used it to modify some parameters, such as `AppId`, `version number`, etc. Now we want to import different SDK dependencies and specify different code through it.

First, two different flavors were created.

```Groovy

    flavorDimensions "flavor_iap"
    productFlavors {
        google {
            dimension "flavor_iap"
            signingConfig signingConfigs.releaseGoogle
        }

        huawei {
            dimension "favor_iap"
            signingConfig signingConfigs.releaseHuawei
      
```

Then configure different `sourceSets` for them.

```Groovy

    sourceSets {
        google {
            kotlin.srcDirs = ["src/google"]
        }
        huawei {
            kotlin.srcDirs = ["src/huawei"]
        }
    }

```

Finally, associate the dependency with the flavor, i.e. prefix `implementation` with the name of the flavor and use the lower camel case.

```groovy
dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    huaweiImplementation 'com.huawei.hms:appservice:5.0.4.302'
}
```

#### Configure source code
The `sourceSets` above configures different code sets, and these specified code sets will be merged with the code under `src/main` - **Note that it is a merge and not a replacement**.

1. The first step is to move the code that would conflict under the original `/src/main` to its respective flavor directory.

Since there is only `MainActivity.kt` file in the directory (`src/main/kotlin/com/... /... `) in my Flutter project, just leave the folder empty after moving the file away.

Also create `google/kotlin/com/... /...`  and `huawei/kotlin/com/... /...` directories under `src` path, with the same structure as the original `src/main`.

2. The second step is to create your own `MainActivity.kt` file in each of the two directories, and then just implement your own functionality depending on the demands.

### Flutter configuration

The main principle of the implementation under Flutter is that it can specify different entry files with the following compilation parameters.

```bash
flutter build apk --help
....
-t, --target=<path> The main entry-point file of the application, as run on the
                                    device.
                                    If the "--target" option is omitted, but a file name is provided
                                    If the "--target" option is omitted, but a file name is provided on the command line, then that is used instead.
                                    (defaults to "lib/main.dart")
```

In addition, it also supports specifying flavors that can be passed through to gradle: 

```bash
--flavor Build a custom app flavor as defined by platform-specific build
                                    setup.
```

Since it supports different entry files, the code needs to do some processing to separate the ``main.dart`` file.

#### Code structure modification

I renamed the original `main.dart` to `daremote.dart` (the name of the main widget), removed the `main` function, and created a `main.google.dart` file and a `main.huawei.dart` file in the lib directory.

```
lib
├── daremote.dart
├── main.google.dart
├── main.huawei.dart
```

#### compile script
Since there is no good way to "conditionally control" the packages listed in `pubspec.yaml`, for example, there is no way to create an entry like `google_dependencies`, so the only way to control it is with the command `flutter pub remove`:

```bash
flutter pub remove --offline in_app_purchase
flutter build appbundle --release --flavor=huawei -t lib/main.huawei.dart
flutter pub add --offline in_app_purchase
```

To compile for huawei, remove google's `in_app_purchase` package first, then in the `flutter build` command, specify the flavor via `--flavor` parameter, specify the `main` file via `-t`, and add the package back after the compilation is completed.

### vscode configuration
In development, the project needs to be compiled and debugged, in vscode, we can set up vscode's `launch.json`. The important thing to note is to configure the `program` field to specify the entry file:

```json
"configurations": [
    {
        "name": "huawei",
        "request": "launch",
        "type": "dart",
        "program": "lib/main.huawei.dart",
        "args": [
            "--flavor",
            "huawei",
            "-t",
            "lib/main.huawei.dart"
        ]
    },
    {
        "name": "google",
        "request": "launch",
        "type": "dart",
        "program": "lib/main.google.dart",
        "args": [
            "--flavor",
            "google",
            "-t",
            "lib.main.google.dart"
        ]
    }
]
```

### Finally
For interpreted languages, it's fine, but for compiled languages, it's really inconvenient not to have "conditional compilation". In order to achieve the same purpose, a lot of extra work has to be done, most notably in a piecemeal fashion.

At one point, I had thought to control all processes by bash scripts, but finally took some time to study how to achieve the purpose by using the features provided by the framework itself.