Flutter Gradle Plugin (FGP) Architecture & Development Rules
Scope & Legacy Code Policy (The Ratchet Principle)
- New Code & Modified Lines: All newly introduced tasks, properties, build logic, and modified lines must strictly comply with these rules.
- No Automatic Legacy Refactoring: Pre-existing code that violates target architectural guidelines (such as Worker API adoption, configuration-cache compliance, or legacy mock boilerplate) should not be automatically refactored within an unrelated PR to prevent scope explosion, high review burden, and regression risks.
- Surface Opportunities to the User: When encountering adjacent legacy violations or cleanup opportunities during development, bring them to the user's attention with a brief rationale rather than silently skipping them or applying unapproved refactors. The user can then decide whether to include a localized cleanup or track it for a follow-up PR.
- Dedicated Refactoring: Large-scale migrations of existing tasks to modern Gradle APIs should be planned and executed in dedicated, standalone pull requests.
1. Zero Pollution of Customer-Facing Build Scripts
- Consumer Build Isolation:
build.gradle.kts is evaluated directly in customer projects via composite builds (includeBuild). Keep build scripts minimal with only logic needed to compile and package the plugin. - No Contributor Tasks in Build Scripts: Never register contributor verification tasks, custom lint tasks, or lifecycle hooks (
check.dependsOn, test.dependsOn) in customer-evaluated build scripts. - Offline Verification: Implement all contributor assertions, bytecode validations, and ABI checks in unit tests (
src/test/kotlin/) or CI integration test shards (packages/flutter_tools/test/integration.shard/).
2. Type-Safe AGP & Gradle API Usage (No @Suppress)
- No Unsafe Casts or Suppression: Never use raw wildcard casts (e.g.,
as NamedDomainObjectContainer<Any>) or @Suppress("UNCHECKED_CAST"). Unchecked casts conceal breaking changes across AGP versions. - Use Idiomatic Type-Safe APIs: Use official, type-safe AGP APIs (e.g.,
pluginBuildTypes.create(name) { initWith(source) }) to manipulate domain objects safely across AGP releases. - Structured Compatibility Layers: When bridging binary- or DSL-incompatible AGP versions (e.g., AGP 8 vs 9), introduce explicit, type-safe abstraction wrappers or reflection bridges.
3. Lazy Configuration & Execution Avoidance
- Never Eagerly Resolve at Configuration Time: Never call .get() or .getOrNull() during plugin application or task configuration (afterEvaluate, task creation). Wire inputs and outputs lazily using Property, Provider, ListProperty, MapProperty, DirectoryProperty, and RegularFileProperty.
- Wire Providers Directly: Pass providers directly into task inputs (e.g.,
task.inputDir.set(extension.path)).
4. Strict Configuration Cache Compatibility
- No Project State in Tasks: Tasks must never hold references to
Project, SourceSet, Configuration, or other non-serializable Gradle model objects in fields or action closures. - Pass Serializable Inputs: Inject required values as primitive types, serializable data structures, or Gradle Property instances annotated with @Input, @InputFiles, or @InputDirectory.
- Use Injected Services: Use Gradle service injection (e.g., @Inject for FileSystemOperations, ArchiveOperations, or ExecOperations) inside tasks instead of calling Project helper methods.
5. Build Avoidance & Path Normalization
- Path Normalization: Annotate all file inputs (such as @InputFile or @InputDirectory) with @PathSensitive(PathSensitivity.RELATIVE) or @PathSensitive(PathSensitivity.NAME_ONLY) (instead of absolute paths) to ensure cache hits across different machines and CI environments.
- Deterministic Cache Keys: Never mark non-deterministic inputs (such as timestamps or machine-dependent environment variables) as
@Input. - Explicit Outputs: Declare
@OutputFile or @OutputDirectory for every produced artifact.
6. Worker API for Heavy Computation
- Thread & Classloader Isolation: Offload heavy computation, class parsing, code generation, or external process execution to
WorkerExecutor / WorkQueue (using noIsolation(), classLoaderIsolation(), or processIsolation()) to keep the main Gradle daemon thread non-blocking.
7. Namespace & Environment Hygiene
- Namespace Internal Properties: Prefix all internal Gradle properties, project extensions, and system properties with
flutter.internal. (e.g., flutter.internal.agpVersion) to prevent collisions with customer app configurations. - Matrix Verification: Verify compatibility against the full matrix of supported AGP versions (AGP 8.x through 9.x) and Gradle versions.