Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

### Improvements

* Android ProGuard/R8 rules can now be extended from `pyproject.toml` via `[tool.flet.android].proguard_rules`. The generated project's `android/app/proguard-rules.pro` was a fixed template file, so an app that needed an extra keep rule had no way to add one short of downloading the published build template, patching the file and passing `--template`. This matters for [Pyjnius](https://flet.dev/blog/tap-into-native-android-and-ios-apis-with-Pyjnius-and-pyobjus): `autoclass()` resolves Java classes by name at runtime, and R8 renames anything in the APK that isn't kept — so `autoclass()` on a class bundled by a Flutter plugin or by your own Java/Kotlin fails in release builds. It fails hard: JNI `FindClass` returns null and the process aborts with `JNI DETECTED ERROR IN APPLICATION: obj == null` / `SIGABRT` rather than raising a catchable Python exception, and because R8 only runs in release builds it never reproduces in debug. Android framework classes (`android.os.Build` and friends) live outside the APK and never needed a rule. Rules are *appended* to the defaults, since R8 has no directive that undoes a keep; to remove the defaults instead — in particular `-keepnames class * { *; }`, which keeps every class and member name in the app and costs 2.5 MB of `classes.dex` on Flet Studio (5.9 MB → 3.4 MB, -43%) — set `[tool.flet.android].proguard_default_rules = false`. Dropping the defaults is safe for Pyjnius's `PythonActivity` access, because `serious_python_android` 4.1.0+ ships that keep rule in its own `consumer-rules.pro`. Defaults are unchanged, so existing builds render exactly the same file by @FeodorFitsner.

* Android Gradle properties can now be configured from `pyproject.toml` via `[tool.flet.android.gradle_properties]`. The generated project's `android/gradle.properties` was previously fixed, so its memory settings — `org.gradle.jvmargs=-Xmx8G` plus a 4 GB metaspace — could not be changed. That is larger than the total RAM of a standard GitHub-hosted runner (measured: 7.8 GB with 3 GB of swap), so release builds, which additionally run Dart AOT once per ABI and R8, could exhaust memory and stall with no error; the only workaround was to download the published build template, patch the file and pass `--template`. Entries in the table override the defaults or add new properties, e.g. `"org.gradle.jvmargs" = "-Xmx3G -XX:MaxMetaspaceSize=1G"` and `"org.gradle.workers.max" = 2`. Defaults are unchanged, so existing builds render exactly the same file by @FeodorFitsner.

## 0.86.4
Expand Down
29 changes: 29 additions & 0 deletions sdk/python/packages/flet-cli/src/flet_cli/commands/build_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -971,6 +971,29 @@ def setup_template_data(self):
),
"android.useAndroidX": "true",
}
# ProGuard/R8 rules for the generated Android project. Like
# gradle_properties above, these were a fixed template file and the
# defaults reproduce it exactly. R8 renames classes in release builds
# while pyjnius resolves them by name, so an app that reaches into a
# bundled AAR needs a keep rule and had no way to add one.
#
# `proguard_rules` appends, since R8 has no directive that undoes a
# keep. Removing a default therefore needs its own switch:
# `proguard_default_rules = false` drops them, which is the only way to
# shed `-keepnames class * { *; }`. Keeping the two separate stops
# users pasting today's defaults into pyproject.toml and silently
# holding them after the defaults change.
keep_defaults = (
self.get_pyproject("tool.flet.android.proguard_default_rules") is not False
)
android_proguard_rules = (
[
"-keep class com.flet.serious_python_android.** { *; }",
"-keepnames class * { *; }",
]
if keep_defaults
else []
)

# merge values from "--permissions" arg:
for p in (
Expand Down Expand Up @@ -1057,6 +1080,11 @@ def setup_template_data(self):
self.get_pyproject("tool.flet.android.gradle_properties") or {},
)

android_proguard_rules = android_proguard_rules + [
str(rule)
for rule in (self.get_pyproject("tool.flet.android.proguard_rules") or [])
]

# parse --android-permissions
for p in self.options.android_permissions:
i = p.find("=")
Expand Down Expand Up @@ -1407,6 +1435,7 @@ def _xml_attr_value(v):
"android_features": android_features,
"android_meta_data": android_meta_data,
"android_gradle_properties": android_gradle_properties,
"android_proguard_rules": android_proguard_rules,
"android_providers": android_providers,
"deep_linking": {
"scheme": deep_linking_scheme,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
-keep class com.flet.serious_python_android.** { *; }
-keepnames class * { *; }
{% for rule in cookiecutter.options.android_proguard_rules %}{{ rule }}
{% endfor %}
93 changes: 93 additions & 0 deletions website/docs/publish/android.md
Original file line number Diff line number Diff line change
Expand Up @@ -935,6 +935,99 @@ adaptive_icon_background = "#0B6BFF"
</TabItem>
</Tabs>

## ProGuard / R8 rules

Release builds run [R8](https://developer.android.com/build/shrink-code), which removes unused
classes and renames the ones that remain. Anything looked up by name at runtime — through
reflection or JNI — has to be exempted with a *keep rule*, or the lookup fails against a class that
no longer has the name it is being asked for.

Flet generates `android/app/proguard-rules.pro` with these defaults:

```
-keep class com.flet.serious_python_android.** { *; }
-keepnames class * { *; }
```

There are two knobs, because adding and removing rules are different problems:

- **`[tool.flet.android].proguard_rules`** — a list of rules *appended* to the defaults. R8 has no
directive that undoes a keep, so a rule you add can only ever widen what is kept.
- **`[tool.flet.android].proguard_default_rules`** — set to `false` to drop the defaults entirely.
This is the only way to get rid of `-keepnames class * { *; }`.

Appending is kept separate from replacing on purpose. If the only option were to replace, you would
paste today's defaults into your `pyproject.toml` and silently keep them after Flet changes them.

### Dropping the defaults

`-keepnames class * { *; }` keeps every class *and member* name in your app, which switches off
obfuscation app-wide and blocks the R8 passes that rely on renaming. It costs real size — on Flet
Studio, removing it takes `classes.dex` from 5.9 MB to 3.4 MB (**-43%**).

Dropping the defaults does **not** break Pyjnius's access to the app activity:
`serious_python_android` 4.1.0+ ships `-keep class com.flet.serious_python_android.** { *; }` in its
own `consumer-rules.pro`, so that rule applies whether or not the template repeats it.

```toml
[tool.flet.android]
proguard_default_rules = false
proguard_rules = [
# add back names your own code looks up reflectively
"-keepnames class com.example.myapp.** { *; }",
]
```

Test a release build on a device before shipping this — see the warning below.

### When you need this

The common case is [Pyjnius](https://flet.dev/blog/tap-into-native-android-and-ios-apis-with-Pyjnius-and-pyobjus).
`autoclass()` resolves Java classes by their fully-qualified name at runtime, so a class R8 renamed
can no longer be found:

- **Android framework classes** (`android.os.Build`, `android.bluetooth.BluetoothAdapter`, …) need
no rule. They live in the Android runtime, not in your APK, so R8 never touches them.
- **Classes bundled by a Flutter plugin or your own Java/Kotlin** are in your APK and *are* renamed.
These need a keep rule.

:::warning A failed lookup crashes the process
`autoclass()` on a renamed class does not raise a Python exception you can catch. JNI `FindClass`
returns null and the process aborts. On a debuggable build the log shows:

```
JNI DETECTED ERROR IN APPLICATION: obj == null in call to CallObjectMethodA
Fatal signal 6 (SIGABRT)
```

On a production build there is usually no message at all — just a crash. Because R8 only runs in
release builds, this never reproduces in debug.
:::

### Example

```toml
[tool.flet.android]
proguard_rules = [
"-keep class io.flutter.embedding.android.FlutterActivity { *; }",
"-keep class com.example.mylib.** { *; }",
]
```

<details>
<summary>Template translation</summary>

In [`android/app/proguard-rules.pro`](index.md#build-template), the `pyproject.toml` example above
will be translated accordingly into this:

```
-keep class com.flet.serious_python_android.** { *; }
-keepnames class * { *; }
-keep class io.flutter.embedding.android.FlutterActivity { *; }
-keep class com.example.mylib.** { *; }
```
</details>

## Extract packages

On Android, pure Python code is packaged into stored zip assets. On first launch, Flet copies the
Expand Down
Loading