diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 236247d2..c6cf5c9c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -70,14 +70,14 @@ jobs: with: distribution: temurin java-version: "17" - # Sample app: compileSdk/targetSdk 33 (see application/build.gradle.kts). + # Sample app: compileSdk 34 (Compose AAR metadata), targetSdk 33. # setup-android accepts licenses, installs cmdline-tools + listed packages, # and exports ANDROID_HOME / ANDROID_SDK_ROOT for AGP. - name: Set up Android SDK uses: android-actions/setup-android@v3 with: # Space-separated sdkmanager package ids (semicolons inside package names). - packages: "platform-tools platforms;android-33 build-tools;33.0.2 build-tools;34.0.0" + packages: "platform-tools platforms;android-34 platforms;android-33 build-tools;33.0.2 build-tools;34.0.0" - name: Set up Gradle uses: gradle/actions/setup-gradle@v4 - name: Build application diff --git a/README.md b/README.md index 69ecd706..bde691ed 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ buildscript { project = project, minSdk = 21, targetSdk = 33, - compileSdk = 33, + compileSdk = 34, agpVersion = "8.1.2", extraPlugins = listOf( "androidx.navigation:navigation-safe-args-gradle-plugin:2.7.4", @@ -99,7 +99,9 @@ androidLibrary( ## Development environment -Worker / contributor host setup (JDK 17+, Android SDK platform 33): see [`docs/ENV.md`](docs/ENV.md) and `source scripts/env-mac.sh`. +Worker / contributor host setup (JDK 17+, Android SDK platform **34** for +`compileSdk`, 33 still useful for target): see [`docs/ENV.md`](docs/ENV.md) and +`source scripts/env-mac.sh`. ## External dependency catalogs @@ -132,6 +134,7 @@ project-dep type checks + content layout rules from live validators | `androidLibrary` | ✅ | Android library | name + project-dep list (**no** `impl`) | | `uiLibrary` | ✅ | Shared UI library for impl/widget | project-dep list | | `widget` | ✅ | Custom View / UI component | project-dep list | +| `composeWidget` | ✅ | Compose UI component | always Compose + project-dep list | | `androidRes` | ✅ | Resources only | only `res/` under `src/main` + project-dep list | | `viewBinding` | ✅ | Layout-only view binding module | only `layout*` under `res` + project-dep list | | `androidTestUtil` | ✅ | Shared code for Android tests | project-dep list | @@ -153,22 +156,25 @@ That document is generated from each target’s | Consumer | May depend on project suffixes… | |----------|----------------------------------| | `api` | `api`, `library` | -| `impl` | `api`, `android-util`, `test-util`, `util`, `library`, `ui-library`, `res`, `viewbinding`, `widget` | +| `impl` | `api`, `android-util`, `test-util`, `util`, `library`, `ui-library`, `res`, `viewbinding`, `widget`, `compose-widget` | | `library` (JVM) | `util`, `test-util` | | `androidLibrary` | `library`, `util`, `android-util`, `test-util`, `res`, `api` | -| `uiLibrary` | `widget`, `util`, `android-util`, `res` | +| `uiLibrary` | `widget`, `compose-widget`, `util`, `android-util`, `res` | | `util` | `util`, `library` | | `androidUtil` | `android-util`, `test-util`, `res` | | `testUtil` | `test-util`, `util` | | `androidTestUtil` | `android-test-util`, `test-util` | -| `androidRes` | `res`, `widget` | -| `widget` | `ui-library`, `widget`, `util`, `android-util`, `res` | -| `viewBinding` | `api`, `widget`, `res`, `library`, `android-util` | -| `androidApp` | `api`, `impl`, `library`, `util`, `android-util`, `test-util`, `res`, `viewbinding`, `widget`, `ui-library` | -| `androidBinary` | `app`, `api`, `impl`, `library`, `util`, `android-util`, `test-util`, `res`, `viewbinding`, `widget`, `ui-library` | +| `androidRes` | `res`, `widget`, `compose-widget` | +| `widget` | `ui-library`, `widget`, `compose-widget`, `util`, `android-util`, `res` | +| `composeWidget` | `ui-library`, `compose-widget`, `widget`, `util`, `android-util`, `res` | +| `viewBinding` | `api`, `widget`, `compose-widget`, `res`, `library`, `android-util` | +| `androidApp` | `api`, `impl`, `library`, `util`, `android-util`, `test-util`, `res`, `viewbinding`, `widget`, `compose-widget`, `ui-library` | +| `androidBinary` | `app`, `api`, `impl`, `library`, `util`, `android-util`, `test-util`, `res`, `viewbinding`, `widget`, `compose-widget`, `ui-library` | | `androidNative` | *(no project-dep check)* | Dagger2-friendly `api`/`impl` + composition roots: **F-011** (landed). +Jetpack Compose: per-target `compose` flag + `composeWidget` — **F-013** +([`docs/COMPOSE.md`](docs/COMPOSE.md)). Icons made by Freepik from www.flaticon.com diff --git a/TICKETS.md b/TICKETS.md index 59d5c849..d56a165c 100644 --- a/TICKETS.md +++ b/TICKETS.md @@ -20,7 +20,7 @@ Update this file when picking or finishing work. Cron workers must pick the **hi | F-010 | done | Document strict dependency matrix from live code (not only README) | `docs/DEPENDENCY-MATRIX.md` + README summary aligned to validators | | F-011 | done | Tighten validation for `api` / `impl` (Dagger2-friendly boundaries) | app/binary/androidLibrary no longer EmptyValidator; impl still no→impl; docs matrix updated | | F-012 | done | External deps catalog UX + tooling polish | `library()` + pure generators + GAV validation + unit tests + `docs/DEPS-CATALOG.md` | -| F-013 | todo | Compose support for Android library/ui targets | GH #96 | +| F-013 | done | Compose support for Android library/ui targets | GH #96; `compose` flags + `composeWidget`; `docs/COMPOSE.md` | | F-014 | todo | Sample app: gold-standard multi-feature structure | Home/characters already present; modernize | | F-015 | todo | Android project tutorial (getting started) | GH #53 | | F-016 | todo | Plugin publish path (Portal user + target publish config) | GH #132, #133 | diff --git a/application/binary/build.gradle.kts b/application/binary/build.gradle.kts index a00a7185..5998a49c 100644 --- a/application/binary/build.gradle.kts +++ b/application/binary/build.gradle.kts @@ -3,6 +3,8 @@ androidBinary( owner = Teams.core, versionCode = 1, versionName = "0.0.1", + // Compose enabled so binary can host Compose content from compose-widget deps. + compose = true, dependencies = deps( target(":root-app"), @@ -19,6 +21,7 @@ androidBinary( target(":common:extensions:android-util"), // target(":common:util-native"), + target(":common:greeting:compose-widget"), target(":core:mvvm:library"), target(":core:di:library") ) diff --git a/application/build.gradle.kts b/application/build.gradle.kts index 2b16bd1f..ec04535b 100644 --- a/application/build.gradle.kts +++ b/application/build.gradle.kts @@ -3,7 +3,8 @@ buildscript { project = rootProject, minSdk = 21, targetSdk = 33, - compileSdk = 33, + // compileSdk 34 required by Compose transitive emoji2 1.4+ AAR metadata + compileSdk = 34, agpVersion = "8.1.2", extraPlugins = listOf( diff --git a/application/common/greeting/compose-widget/build.gradle.kts b/application/common/greeting/compose-widget/build.gradle.kts new file mode 100644 index 00000000..2b7e6a23 --- /dev/null +++ b/application/common/greeting/compose-widget/build.gradle.kts @@ -0,0 +1,6 @@ +composeWidget( + packageName = "tools.forma.sample.common.greeting.compose.widget", + dependencies = deps( + androidx.compose + ) +) diff --git a/application/common/greeting/compose-widget/src/main/java/tools/forma/sample/common/greeting/compose/widget/GreetingCard.kt b/application/common/greeting/compose-widget/src/main/java/tools/forma/sample/common/greeting/compose/widget/GreetingCard.kt new file mode 100644 index 00000000..f4074625 --- /dev/null +++ b/application/common/greeting/compose-widget/src/main/java/tools/forma/sample/common/greeting/compose/widget/GreetingCard.kt @@ -0,0 +1,23 @@ +package tools.forma.sample.common.greeting.compose.widget + +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Card +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +/** + * Minimal Compose UI sample for Forma `composeWidget` target (F-013 / GH #96). + */ +@Composable +fun GreetingCard(name: String, modifier: Modifier = Modifier) { + Card(modifier = modifier.padding(8.dp), elevation = 4.dp) { + Text( + text = "Hello, $name!", + style = MaterialTheme.typography.h6, + modifier = Modifier.padding(16.dp) + ) + } +} diff --git a/build-dependencies/dependencies/src/main/kotlin/Androidx.kt b/build-dependencies/dependencies/src/main/kotlin/Androidx.kt index 80d72ea1..ae2b68f3 100644 --- a/build-dependencies/dependencies/src/main/kotlin/Androidx.kt +++ b/build-dependencies/dependencies/src/main/kotlin/Androidx.kt @@ -296,4 +296,20 @@ object androidx { navigation_ui_ktx, navigation_common_ktx ) + + /** + * Jetpack Compose runtime + UI + foundation + material (pinned versions). + * Pair with `compose = true` / `composeWidget` targets (F-013). + * Versions match Compose Compiler 1.5.3 / Kotlin 1.9.10. + * Uses [transitiveDeps] so Compose UI unit/graphics transitively resolve. + * Does not include activity-compose (pulls emoji2 requiring compileSdk 34); + * add that separately when hosting Compose in Activities on SDK 34+. + */ + val compose = transitiveDeps( + "androidx.compose.runtime:runtime:${versions.androidx.compose}", + "androidx.compose.ui:ui:${versions.androidx.compose}", + "androidx.compose.foundation:foundation:${versions.androidx.compose}", + "androidx.compose.material:material:${versions.androidx.compose}", + "androidx.compose.ui:ui-tooling-preview:${versions.androidx.compose}", + ) } diff --git a/build-dependencies/dependencies/src/main/kotlin/Versions.kt b/build-dependencies/dependencies/src/main/kotlin/Versions.kt index e83c342f..5fe20819 100644 --- a/build-dependencies/dependencies/src/main/kotlin/Versions.kt +++ b/build-dependencies/dependencies/src/main/kotlin/Versions.kt @@ -39,6 +39,9 @@ object versions { const val vectordrawable = "1.1.0" const val versionedparcelable = "1.1.0" const val viewpager = "1.0.0" + // Compose set for sample (Kotlin 1.9.10 / compiler 1.5.3) + // Pin artifacts directly (Forma MixedDependency does not yet carry PlatformSpec) + const val compose = "1.5.3" } object google { diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 89935013..9d1222d4 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -125,6 +125,7 @@ Defined in `plugins/android/.../AndroidTargets.kt`: | `ApiTargetTemplate` | `api` | `api` | | `ImplTargetTemplate` | `impl` | `impl` | | `WidgetTargetTemplate` | `widget` | `widget` | +| `ComposeWidgetTargetTemplate` | `compose-widget` | `composeWidget` | Name rule (`validation`): project name equals `suffix` or ends with `-$suffix`. @@ -137,19 +138,20 @@ when validators change. | Consumer DSL | Allowed *project* dependency suffixes | Content rules | |--------------|----------------------------------------|---------------| | `api` | `api`, `library` | no `res/` under `src/main` | -| `impl` | `api`, `android-util`, `test-util`, `util`, `library`, `ui-library`, `res`, `viewbinding`, `widget` | — | +| `impl` | `api`, `android-util`, `test-util`, `util`, `library`, `ui-library`, `res`, `viewbinding`, `widget`, `compose-widget` | — | | `library` (JVM) | `util`, `test-util` | — | | `androidLibrary` | `library`, `util`, `android-util`, `test-util`, `res`, `api` | — | -| `uiLibrary` | `widget`, `util`, `android-util`, `res` | — | +| `uiLibrary` | `widget`, `compose-widget`, `util`, `android-util`, `res` | — | | `util` | `util`, `library` | no `res/` | | `androidUtil` | `android-util`, `test-util`, `res` | no `res/` | | `testUtil` | `test-util`, `util` | no `res/` | | `androidTestUtil` | `android-test-util`, `test-util` | — | -| `androidRes` | `res`, `widget` | **only** `res/` under `src/main` | -| `widget` | `ui-library`, `widget`, `util`, `android-util`, `res` | — | -| `viewBinding` | `api`, `widget`, `res`, `library`, `android-util` | only `layout*` under `src/main/res` | -| `androidApp` | `api`, `impl`, `library`, `util`, `android-util`, `test-util`, `res`, `viewbinding`, `widget`, `ui-library` | no `res/` | -| `androidBinary` | `app`, `api`, `impl`, `library`, `util`, `android-util`, `test-util`, `res`, `viewbinding`, `widget`, `ui-library` | no `res/` | +| `androidRes` | `res`, `widget`, `compose-widget` | **only** `res/` under `src/main` | +| `widget` | `ui-library`, `widget`, `compose-widget`, `util`, `android-util`, `res` | — | +| `composeWidget` | `ui-library`, `compose-widget`, `widget`, `util`, `android-util`, `res` | always Compose | +| `viewBinding` | `api`, `widget`, `compose-widget`, `res`, `library`, `android-util` | only `layout*` under `src/main/res` | +| `androidApp` | `api`, `impl`, `library`, `util`, `android-util`, `test-util`, `res`, `viewbinding`, `widget`, `compose-widget`, `ui-library` | no `res/` | +| `androidBinary` | `app`, `api`, `impl`, `library`, `util`, `android-util`, `test-util`, `res`, `viewbinding`, `widget`, `compose-widget`, `ui-library` | no `res/` | | `androidNative` | (no `applyDependencies` in current code) | no `res/` | Notes for later tickets: @@ -169,7 +171,8 @@ Under `tools.forma.android.feature`: Configuration singleton: `Forma` object delegates to `FormaSettingsStore` (`AndroidProjectSettings`: min/target/compile SDK, AGP/Kotlin versions, repos, -compose flag, owners mandatory flag, Java compatibility). +compose default + compose compiler version, owners mandatory flag, Java +compatibility). See [`COMPOSE.md`](COMPOSE.md) for F-013 usage. ### 2.4 Deps subsystem @@ -234,7 +237,7 @@ Root configuration (`application/build.gradle.kts`): ```kotlin androidProjectConfiguration( - minSdk = 21, targetSdk = 33, compileSdk = 33, + minSdk = 21, targetSdk = 33, compileSdk = 34, agpVersion = "8.1.2", extraPlugins = [ demo deps, KSP, nav safe-args, crashlytics ] ) @@ -275,7 +278,7 @@ Still optional / later: plugin publish or Plugin Marker validation job; deeper G | Gradle | plugins 8.3, application 8.4 | | AGP | **8.1.2** sample runtime + plugins compile (aligned F-003) | | Kotlin | embeddedKotlin from Gradle distribution | -| Android SDK | sample compile/target 33; host platforms;android-33 + build-tools 33/34 | +| Android SDK | sample compileSdk **34** / target 33; host platforms 34+33 + build-tools 33/34 | | Forma version | 0.1.3 | Host bootstrap details: `docs/ENV.md`, `scripts/env-mac.sh` (F-001). @@ -317,7 +320,7 @@ Suggested extraction order (tickets F-020…F-024): | ~~`EmptyValidator` on app/binary/androidLibrary~~ (composition-root + library allowlists) | F-011 done | | ~~AGP 7.4.2 compile vs 8.1.2 runtime~~ (aligned 8.1.2) | F-003 done | | ~~CI missing SDK + Java on some jobs~~ (GHA green on PR #153) | F-004 done | -| Compose flag in settings, limited target support | F-013 | +| ~~Compose flag in settings, limited target support~~ → per-target flags + `composeWidget` | F-013 done | | Shared `library` suffix for JVM vs Android library | F-020 | | Plugin publish / Portal path | F-016 | | Configuration-time cost (includer walk, stores) | F-017 | diff --git a/docs/COMPOSE.md b/docs/COMPOSE.md new file mode 100644 index 00000000..9d1af288 --- /dev/null +++ b/docs/COMPOSE.md @@ -0,0 +1,108 @@ +# Jetpack Compose support (F-013 / GH #96) + +Forma enables Compose **per target** (with an optional project-wide default), +and provides a dedicated **`composeWidget`** target for Compose UI components. + +## Project-wide settings + +In the root `build.gradle.kts` `buildscript` block: + +```kotlin +androidProjectConfiguration( + project = rootProject, + minSdk = 21, + targetSdk = 33, + // compileSdk 34 when using modern Compose transitive AARs + compileSdk = 34, + agpVersion = "8.1.2", + compose = false, // default for per-target flags + composeCompilerVersion = "1.5.3", // must match Kotlin (1.9.10 → 1.5.3) +) +``` + +| Setting | Role | +|---------|------| +| `compose` | Default value for per-target `compose = …` parameters on `impl`, `androidLibrary`, `androidUtil`, `androidApp`, `uiLibrary`, `androidBinary`. | +| `composeCompilerVersion` | Written to AGP `composeOptions.kotlinCompilerExtensionVersion` when Compose is enabled. | + +`composeWidget` modules **always** enable Compose; they ignore the project default +(they do not need `compose = true`). + +## Per-target `compose` flag + +```kotlin +impl( + packageName = "com.example.feature.home.impl", + compose = true, + dependencies = deps(/* … */), +) + +androidLibrary( + packageName = "com.example.shared.ui.library", + compose = true, +) + +androidBinary( + packageName = "com.example.app", + versionCode = 1, + versionName = "1.0", + compose = true, + dependencies = deps(target(":root-app"), /* … */), +) +``` + +When `compose = true`, Forma sets: + +- `android.buildFeatures.compose = true` +- `android.composeOptions.kotlinCompilerExtensionVersion = ` + +You still declare Compose **libraries** yourself (runtime, UI, material, …) +via catalogs / `deps(...)`. Forma does not inject Compose artifacts automatically. + +## `composeWidget` target + +Suffix: **`compose-widget`** (project name must end with `-compose-widget` or equal +`compose-widget`). + +```kotlin +// application/common/greeting/compose-widget/build.gradle.kts +composeWidget( + packageName = "com.example.common.greeting.compose.widget", + dependencies = deps( + androidx.compose // sample catalog cluster + ) +) +``` + +Parallel to `widget` (View system). View and Compose may depend on each other: + +- `widget` → may depend on `compose-widget` +- `compose-widget` → may depend on `widget` + +so hybrid screens can mix View and Compose (GH #96). + +## Who may depend on `compose-widget` + +| Consumer | `compose-widget` allowed? | +|----------|---------------------------| +| `impl` | yes | +| `androidApp` / `androidBinary` | yes | +| `uiLibrary` | yes | +| `widget` | yes | +| `composeWidget` | yes (self) | +| `androidRes` / `viewBinding` | yes (resources/layout wiring) | +| `api` / JVM `library` / `util` | no | + +Full matrix: [`DEPENDENCY-MATRIX.md`](DEPENDENCY-MATRIX.md). + +## Compiler / Kotlin pairing + +Default `composeCompilerVersion = "1.5.3"` matches Kotlin **1.9.10** (Gradle 8.4 +embedded Kotlin used by the sample). If you bump Kotlin, update +`composeCompilerVersion` using the +[Compose Compiler compatibility map](https://developer.android.com/jetpack/androidx/releases/compose-kotlin). + +## Sample + +`application/common/greeting/compose-widget` — tiny `@Composable` card depended on +by `binary` with `compose = true`. diff --git a/docs/DEPENDENCY-MATRIX.md b/docs/DEPENDENCY-MATRIX.md index ea80540d..5c3dc7d9 100644 --- a/docs/DEPENDENCY-MATRIX.md +++ b/docs/DEPENDENCY-MATRIX.md @@ -8,7 +8,7 @@ Source of truth for **F-010**. Every rule below is taken from the live When validators change, update **this file first**, then the README summary matrix. Do not invent rules from aspirational docs. -Last verified: **2026-07-11** against `v2` tip + F-011 validator tighten. +Last verified: **2026-07-11** against `v2` tip + F-013 Compose support. --- @@ -54,6 +54,7 @@ From `plugins/android/.../AndroidTargets.kt`: | `api` | `ApiTargetTemplate` | `api` | | `impl` | `ImplTargetTemplate` | `impl` | | `widget` | `WidgetTargetTemplate` | `widget` | +| `composeWidget` | `ComposeWidgetTargetTemplate` | `compose-widget` | **Collision note:** JVM `library` and Android `androidLibrary` share the same suffix `library` / `LibraryTargetTemplate`. The DSL functions differ (Kotlin @@ -74,19 +75,20 @@ these suffixes (or are unrestricted if `EmptyValidator`). | Consumer DSL | Source file | Project-dep validator | Allowed dependency suffixes | |--------------|-------------|------------------------|-----------------------------| | `api` | `api.kt` | `validator(api, library)` | `api`, `library` | -| `impl` | `impl.kt` | `validator(api, android-util, test-util, util, library, ui-library, res, viewbinding, widget)` | `api`, `android-util`, `test-util`, `util`, `library`, `ui-library`, `res`, `viewbinding`, `widget` | +| `impl` | `impl.kt` | `validator(api, android-util, test-util, util, library, ui-library, res, viewbinding, widget, compose-widget)` | `api`, `android-util`, `test-util`, `util`, `library`, `ui-library`, `res`, `viewbinding`, `widget`, `compose-widget` | | `library` (JVM) | `library.kt` | `validator(util, test-util)` | `util`, `test-util` | | `androidLibrary` | `androidLibrary.kt` | `validator(library, util, android-util, test-util, res, api)` | `library`, `util`, `android-util`, `test-util`, `res`, `api` | -| `uiLibrary` | `uiLibrary.kt` | `validator(widget, util, android-util, res)` | `widget`, `util`, `android-util`, `res` | +| `uiLibrary` | `uiLibrary.kt` | `validator(widget, compose-widget, util, android-util, res)` | `widget`, `compose-widget`, `util`, `android-util`, `res` | | `util` | `util.kt` | `validator(util, library)` | `util`, `library` | | `androidUtil` | `androidUtil.kt` | `validator(android-util, test-util, res)` | `android-util`, `test-util`, `res` | | `testUtil` | `testUtil.kt` | `validator(test-util, util)` | `test-util`, `util` | | `androidTestUtil` | `androidTestUtil.kt` | `validator(android-test-util, test-util)` | `android-test-util`, `test-util` | -| `androidRes` | `androidRes.kt` | `validator(res, widget)` | `res`, `widget` | -| `widget` | `widget.kt` | `validator(ui-library, widget, util, android-util, res)` | `ui-library`, `widget`, `util`, `android-util`, `res` | -| `viewBinding` | `viewBinding.kt` | `validator(api, widget, res, library, android-util)` | `api`, `widget`, `res`, `library`, `android-util` | -| `androidApp` | `androidApp.kt` | `validator(api, impl, library, util, android-util, test-util, res, viewbinding, widget, ui-library)` | `api`, `impl`, `library`, `util`, `android-util`, `test-util`, `res`, `viewbinding`, `widget`, `ui-library` | -| `androidBinary` | `androidBinary.kt` | `validator(app, api, impl, library, util, android-util, test-util, res, viewbinding, widget, ui-library)` | `app`, `api`, `impl`, `library`, `util`, `android-util`, `test-util`, `res`, `viewbinding`, `widget`, `ui-library` | +| `androidRes` | `androidRes.kt` | `validator(res, widget, compose-widget)` | `res`, `widget`, `compose-widget` | +| `widget` | `widget.kt` | `validator(ui-library, widget, compose-widget, util, android-util, res)` | `ui-library`, `widget`, `compose-widget`, `util`, `android-util`, `res` | +| `composeWidget` | `composeWidget.kt` | `validator(ui-library, compose-widget, widget, util, android-util, res)` | `ui-library`, `compose-widget`, `widget`, `util`, `android-util`, `res` | +| `viewBinding` | `viewBinding.kt` | `validator(api, widget, compose-widget, res, library, android-util)` | `api`, `widget`, `compose-widget`, `res`, `library`, `android-util` | +| `androidApp` | `androidApp.kt` | `validator(api, impl, library, util, android-util, test-util, res, viewbinding, widget, compose-widget, ui-library)` | `api`, `impl`, `library`, `util`, `android-util`, `test-util`, `res`, `viewbinding`, `widget`, `compose-widget`, `ui-library` | +| `androidBinary` | `androidBinary.kt` | `validator(app, api, impl, library, util, android-util, test-util, res, viewbinding, widget, compose-widget, ui-library)` | `app`, `api`, `impl`, `library`, `util`, `android-util`, `test-util`, `res`, `viewbinding`, `widget`, `compose-widget`, `ui-library` | | `androidNative` | `androidNative.kt` | *(no `applyDependencies`)* | *no project-dep validation* | ### Explicit non-edges (important product rules) @@ -105,8 +107,16 @@ These follow from the table (not from separate deny-lists): - **`androidApp` / `androidBinary` cannot depend on other `binary`** — single APK composition root (F-011; `app` may not depend on `app`/`binary`). - **Circular-ish UI graph is intentional while experimental:** `uiLibrary` - may depend on `widget`, and `widget` may depend on `ui-library` - (`uiLibrary.kt` comment). + may depend on `widget` / `compose-widget`, and `widget` / `compose-widget` + may depend on `ui-library` and on each other so View + Compose can coexist + (F-013 / GH #96). + +### Compose flags (not project-dep rules) + +Separate from suffix validation: `impl`, `androidLibrary`, `androidUtil`, +`androidApp`, `uiLibrary`, and `androidBinary` accept `compose: Boolean` +(default = project `androidProjectConfiguration(compose=…)`). +`composeWidget` **always** enables Compose. See [`COMPOSE.md`](COMPOSE.md). --- @@ -141,23 +151,24 @@ Legend: Rows = **consumer**. Columns = **dependency type (suffix)**. -| Consumer ↓ \ Dep → | api | impl | library | ui-library | util | test-util | android-util | android-test-util | res | viewbinding | widget | app | binary | native | -|--------------------|-----|------|---------|------------|------|-----------|--------------|-------------------|-----|-------------|--------|-----|--------|--------| -| **api** | Y | — | Y | — | — | — | — | — | — | — | — | — | — | — | -| **impl** | Y | — | Y | Y | Y | Y | Y | — | Y | Y | Y | — | — | — | -| **library** (JVM) | — | — | — | — | Y | Y | — | — | — | — | — | — | — | — | -| **androidLibrary** | Y | — | Y | — | Y | Y | Y | — | Y | — | — | — | — | — | -| **uiLibrary** | — | — | — | — | Y | — | Y | — | Y | — | Y | — | — | — | -| **util** | — | — | Y | — | Y | — | — | — | — | — | — | — | — | — | -| **androidUtil** | — | — | — | — | — | Y | Y | — | Y | — | — | — | — | — | -| **testUtil** | — | — | — | — | Y | Y | — | — | — | — | — | — | — | — | -| **androidTestUtil** | — | — | — | — | — | Y | — | Y | — | — | — | — | — | — | -| **androidRes** | — | — | — | — | — | — | — | — | Y | — | Y | — | — | — | -| **widget** | — | — | — | Y | Y | — | Y | — | Y | — | Y | — | — | — | -| **viewBinding** | Y | — | Y | — | — | — | Y | — | Y | — | Y | — | — | — | -| **androidApp** | Y | Y | Y | Y | Y | Y | Y | — | Y | Y | Y | — | — | — | -| **androidBinary** | Y | Y | Y | Y | Y | Y | Y | — | Y | Y | Y | Y | — | — | -| **androidNative** | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | +| Consumer ↓ \ Dep → | api | impl | library | ui-library | util | test-util | android-util | android-test-util | res | viewbinding | widget | compose-widget | app | binary | native | +|--------------------|-----|------|---------|------------|------|-----------|--------------|-------------------|-----|-------------|--------|----------------|-----|--------|--------| +| **api** | Y | — | Y | — | — | — | — | — | — | — | — | — | — | — | — | +| **impl** | Y | — | Y | Y | Y | Y | Y | — | Y | Y | Y | Y | — | — | — | +| **library** (JVM) | — | — | — | — | Y | Y | — | — | — | — | — | — | — | — | — | +| **androidLibrary** | Y | — | Y | — | Y | Y | Y | — | Y | — | — | — | — | — | — | +| **uiLibrary** | — | — | — | — | Y | — | Y | — | Y | — | Y | Y | — | — | — | +| **util** | — | — | Y | — | Y | — | — | — | — | — | — | — | — | — | — | +| **androidUtil** | — | — | — | — | — | Y | Y | — | Y | — | — | — | — | — | — | +| **testUtil** | — | — | — | — | Y | Y | — | — | — | — | — | — | — | — | — | +| **androidTestUtil** | — | — | — | — | — | Y | — | Y | — | — | — | — | — | — | — | +| **androidRes** | — | — | — | — | — | — | — | — | Y | — | Y | Y | — | — | — | +| **widget** | — | — | — | Y | Y | — | Y | — | Y | — | Y | Y | — | — | — | +| **composeWidget** | — | — | — | Y | Y | — | Y | — | Y | — | Y | Y | — | — | — | +| **viewBinding** | Y | — | Y | — | — | — | Y | — | Y | — | Y | Y | — | — | — | +| **androidApp** | Y | Y | Y | Y | Y | Y | Y | — | Y | Y | Y | Y | — | — | — | +| **androidBinary** | Y | Y | Y | Y | Y | Y | Y | — | Y | Y | Y | Y | Y | — | — | +| **androidNative** | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | n/a | Self-deps: a type may depend on itself only if its own suffix is in its allowed list (e.g. `api`→`api` **Y**, `impl`→`impl` **—**, `widget`→`widget` @@ -209,5 +220,5 @@ See `docs/ARCHITECTURE.md` §2.2 and §3 for the module map. 3. Refresh the compact README matrix (link here as canonical). 4. Note the change in `docs/PROGRESS.md` / ticket notes when user-facing. -Related tickets: **F-012** (deps catalog UX), **F-020** (forma-core type -registry / shared `library` suffix). +Related tickets: **F-013** (Compose), **F-012** (deps catalog UX), **F-020** +(forma-core type registry / shared `library` suffix). diff --git a/docs/ENV.md b/docs/ENV.md index bc3f72cf..81e1173a 100644 --- a/docs/ENV.md +++ b/docs/ENV.md @@ -1,6 +1,7 @@ # Worker host environment (macOS) -Forma workers need **JDK 17+** and an **Android SDK** with platform 33 (sample app `compileSdk`/`targetSdk`). +Forma workers need **JDK 17+** and an **Android SDK** with platform **34** +(sample `compileSdk`; Compose AAR metadata) and optionally **33** (`targetSdk`). ## Quick start (this Mac) @@ -8,7 +9,8 @@ Forma workers need **JDK 17+** and an **Android SDK** with platform 33 (sample a # Already installed on the Forma worker host (no sudo): # brew install openjdk@17 # brew install --cask android-commandlinetools -# sdk packages: platforms;android-33, platform-tools, build-tools 33.0.2 + 34.0.0 +# sdk packages: platforms;android-34, platforms;android-33, platform-tools, +# build-tools 33.0.2 + 34.0.0 source scripts/env-mac.sh @@ -46,6 +48,7 @@ export PATH="$JAVA_HOME/bin:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME yes | sdkmanager --licenses sdkmanager --install \ + "platforms;android-34" \ "platforms;android-33" \ "platform-tools" \ "build-tools;33.0.2" \ @@ -61,13 +64,14 @@ sudo ln -sfn /usr/local/opt/openjdk@17/libexec/openjdk.jdk \ /Library/Java/JavaVirtualMachines/openjdk-17.jdk ``` -## Verified on 2026-07-10 (F-001) / re-verified 2026-07-11 (F-003) +## Verified on 2026-07-10 (F-001) / re-verified 2026-07-11 (F-003, F-013) - `java` / `javac` 17.0.19 (Homebrew OpenJDK) - `plugins/`: `./gradlew build` → **BUILD SUCCESSFUL** (AGP compile dep **8.1.2**) - `includer/`: `./gradlew build` → **BUILD SUCCESSFUL** - `depgen/`: `./gradlew build` → **BUILD SUCCESSFUL** -- `application/`: `./gradlew build` → **BUILD SUCCESSFUL** (2080 tasks, AGP runtime 8.1.2, Android SDK 33) +- `application/`: `./gradlew build` → **BUILD SUCCESSFUL** (compileSdk **34**, + targetSdk 33, Compose compiler 1.5.3, 2151 tasks) - Gradle wrappers: plugins 8.3, application 8.4 - Toolchain note: plugins compile AGP matches sample forced AGP (no 7.4.2 skew) diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 76726f26..520bd987 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -2,6 +2,33 @@ Newest entries first. +## 2026-07-11 — F-013 Compose support (GH #96) + +- **Ticket:** F-013 → `done` +- **Branch:** `forma/F-013-compose-support` (from `origin/v2`) +- **Plugin / DSL:** + - `AndroidLibraryFeatureConfiguration.compose` + `AndroidBinaryFeatureConfiguration.compose` + → `buildFeatures.compose` + `composeOptions.kotlinCompilerExtensionVersion` + - Per-target `compose` flag (default = project `androidProjectConfiguration(compose=…)`) + on `impl`, `androidLibrary`, `androidUtil`, `androidApp`, `uiLibrary`, `androidBinary` + - New target **`composeWidget`** / suffix `compose-widget` (always Compose) + - Project settings: `composeCompilerVersion` (default **1.5.3** for Kotlin 1.9.10) + - Validators: `compose-widget` allowed from impl/app/binary/uiLibrary/widget/viewBinding/res; + widget ↔ compose-widget mutual deps (View + Compose coexist) +- **Sample:** + - `application/common/greeting/compose-widget` with `@Composable GreetingCard` + - `binary` depends on it with `compose = true` + - `androidx.compose` catalog cluster (`transitiveDeps`) in build-dependencies + - Sample `compileSdk` **34** (Compose emoji2 AAR metadata); `targetSdk` remains 33 +- **Docs:** `docs/COMPOSE.md`; matrix/README/ARCHITECTURE/ENV/CI updated +- **Verify (real tool output, OpenJDK 17 + SDK 34):** + - `plugins/`: `./gradlew build` → **BUILD SUCCESSFUL** + - `application/`: `./gradlew build` → **BUILD SUCCESSFUL** (2151 tasks) + - `:common-greeting-compose-widget:compileDebugKotlin` + `:binary:assembleDebug` green +- **Commits/PRs:** this run — push + PR base `v2` +- **Blockers:** none +- **Next step:** F-014 sample app gold-standard multi-feature structure + ## 2026-07-11 — F-012 External deps catalog UX - **Ticket:** F-012 → `done` diff --git a/plugins/android/src/main/java/androidApp.kt b/plugins/android/src/main/java/androidApp.kt index bb2b98fa..71cb4dfe 100644 --- a/plugins/android/src/main/java/androidApp.kt +++ b/plugins/android/src/main/java/androidApp.kt @@ -13,6 +13,7 @@ import tools.forma.android.target.UiLibraryTargetTemplate import tools.forma.android.target.UtilTargetTemplate import tools.forma.android.target.ViewBindingTargetTemplate import tools.forma.android.target.WidgetTargetTemplate +import tools.forma.android.target.ComposeWidgetTargetTemplate import tools.forma.owners.NoOwner import tools.forma.owners.Owner import tools.forma.android.utils.BuildConfiguration @@ -44,7 +45,9 @@ fun Project.androidApp( testInstrumentationRunner: String = androidJunitRunner, buildConfiguration: BuildConfiguration = BuildConfiguration(), consumerMinificationFiles: Set = emptySet(), - manifestPlaceholders: Map = emptyMap() + manifestPlaceholders: Map = emptyMap(), + /** Enable Jetpack Compose; defaults to project-wide `compose` setting. */ + compose: Boolean = Forma.settings.compose, ) { disallowResources() @@ -55,7 +58,8 @@ fun Project.androidApp( buildConfiguration, testInstrumentationRunner, consumerMinificationFiles, - manifestPlaceholders + manifestPlaceholders, + compose = compose, ) applyFeatures( androidLibraryFeatureDefinition(libraryFeatureConfiguration), @@ -73,6 +77,7 @@ fun Project.androidApp( ResourcesTargetTemplate, ViewBindingTargetTemplate, WidgetTargetTemplate, + ComposeWidgetTargetTemplate, UiLibraryTargetTemplate, ), dependencies = dependencies, diff --git a/plugins/android/src/main/java/androidBinary.kt b/plugins/android/src/main/java/androidBinary.kt index ab9ea451..a19b0c80 100644 --- a/plugins/android/src/main/java/androidBinary.kt +++ b/plugins/android/src/main/java/androidBinary.kt @@ -16,6 +16,7 @@ import tools.forma.android.target.UiLibraryTargetTemplate import tools.forma.android.target.UtilTargetTemplate import tools.forma.android.target.ViewBindingTargetTemplate import tools.forma.android.target.WidgetTargetTemplate +import tools.forma.android.target.ComposeWidgetTargetTemplate import tools.forma.android.utils.BuildConfiguration import tools.forma.android.validation.disallowResources import tools.forma.deps.core.FormaDependency @@ -47,7 +48,9 @@ fun Project.androidBinary( buildConfiguration: BuildConfiguration = BuildConfiguration(), testInstrumentationRunner: String = androidJunitRunner, consumerMinificationFiles: Set = emptySet(), - manifestPlaceholders: Map = emptyMap() + manifestPlaceholders: Map = emptyMap(), + /** Enable Jetpack Compose; defaults to project-wide `compose` setting. */ + compose: Boolean = Forma.settings.compose, ): TargetBuilder { disallowResources() @@ -61,7 +64,8 @@ fun Project.androidBinary( buildConfiguration, testInstrumentationRunner, consumerMinificationFiles, - manifestPlaceholders + manifestPlaceholders, + compose = compose, ) applyFeatures( androidBinaryFeatureDefinition(binaryFeatureConfiguration) @@ -79,6 +83,7 @@ fun Project.androidBinary( ResourcesTargetTemplate, ViewBindingTargetTemplate, WidgetTargetTemplate, + ComposeWidgetTargetTemplate, UiLibraryTargetTemplate, ), dependencies = dependencies, diff --git a/plugins/android/src/main/java/androidLibrary.kt b/plugins/android/src/main/java/androidLibrary.kt index 31039256..8a64c316 100644 --- a/plugins/android/src/main/java/androidLibrary.kt +++ b/plugins/android/src/main/java/androidLibrary.kt @@ -40,6 +40,8 @@ fun Project.androidLibrary( buildConfiguration: BuildConfiguration = BuildConfiguration(), consumerMinificationFiles: Set = emptySet(), manifestPlaceholders: Map = emptyMap(), + /** Enable Jetpack Compose; defaults to project-wide `compose` setting. */ + compose: Boolean = Forma.settings.compose, ): TargetBuilder { target.validate(LibraryTargetTemplate) val libraryFeatureConfiguration = AndroidLibraryFeatureConfiguration( @@ -48,6 +50,7 @@ fun Project.androidLibrary( testInstrumentationRunner, consumerMinificationFiles, manifestPlaceholders, + compose = compose, ) applyFeatures( androidLibraryFeatureDefinition(libraryFeatureConfiguration), diff --git a/plugins/android/src/main/java/androidProjectConfiguration.kt b/plugins/android/src/main/java/androidProjectConfiguration.kt index c8786594..7eff29c0 100644 --- a/plugins/android/src/main/java/androidProjectConfiguration.kt +++ b/plugins/android/src/main/java/androidProjectConfiguration.kt @@ -24,6 +24,8 @@ import tools.forma.config.SettingsStore * @param javaVersionCompatibility is a java version that will be used for targetCompatibility and * sourceCompatibility versions * @param mandatoryOwners is a flag that enables mandatory owners for all modules + * @param compose project-wide default for per-target Compose flags (see [AndroidProjectSettings.compose]) + * @param composeCompilerVersion Compose compiler extension version for AGP `composeOptions` * @param extraPlugins is a list of extra plugins that will be applied to project */ fun ScriptHandlerScope.androidProjectConfiguration( @@ -35,6 +37,7 @@ fun ScriptHandlerScope.androidProjectConfiguration( agpVersion: String, repositories: RepositoryHandler.() -> Unit = {}, compose: Boolean = false, + composeCompilerVersion: String = DEFAULT_COMPOSE_COMPILER_VERSION, javaVersionCompatibility: JavaVersion = JavaVersion.VERSION_1_8, // Java/Kotlin configuration mandatoryOwners: Boolean = false, vectorDrawablesUseSupportLibrary: Boolean = false, @@ -67,6 +70,7 @@ fun ScriptHandlerScope.androidProjectConfiguration( javaVersionCompatibility = javaVersionCompatibility, mandatoryOwners = mandatoryOwners, compose = compose, + composeCompilerVersion = composeCompilerVersion, vectorDrawablesUseSupportLibrary = vectorDrawablesUseSupportLibrary ) @@ -87,6 +91,7 @@ fun Project.androidProjectConfiguration( javaVersionCompatibility: JavaVersion = JavaVersion.VERSION_1_8, // Java/Kotlin configuration mandatoryOwners: Boolean = false, compose: Boolean = false, + composeCompilerVersion: String = DEFAULT_COMPOSE_COMPILER_VERSION, vectorDrawablesUseSupportLibrary: Boolean = true, ) { @@ -106,12 +111,16 @@ fun Project.androidProjectConfiguration( javaVersionCompatibility = javaVersionCompatibility, mandatoryOwners = mandatoryOwners, compose = compose, + composeCompilerVersion = composeCompilerVersion, vectorDrawablesUseSupportLibrary = vectorDrawablesUseSupportLibrary ) Forma.store(configuration) } +/** Compose Compiler matching Kotlin 1.9.10 (application Gradle 8.4 embedded Kotlin). */ +const val DEFAULT_COMPOSE_COMPILER_VERSION = "1.5.3" + val buildScriptConfiguration: ScriptHandlerScope.(List) -> Unit = { classpathDeps -> // TODO pass repositories configuration repositories { diff --git a/plugins/android/src/main/java/androidRes.kt b/plugins/android/src/main/java/androidRes.kt index 1a25f2e3..6f25cbf9 100644 --- a/plugins/android/src/main/java/androidRes.kt +++ b/plugins/android/src/main/java/androidRes.kt @@ -4,6 +4,7 @@ import tools.forma.android.feature.applyFeatures import tools.forma.android.feature.kotlinAndroidFeatureDefinition import tools.forma.android.target.ResourcesTargetTemplate import tools.forma.android.target.WidgetTargetTemplate +import tools.forma.android.target.ComposeWidgetTargetTemplate import tools.forma.owners.NoOwner import tools.forma.owners.Owner import tools.forma.validation.validate @@ -39,7 +40,8 @@ fun Project.androidRes( applyDependencies( validator = validator( ResourcesTargetTemplate, - WidgetTargetTemplate + WidgetTargetTemplate, + ComposeWidgetTargetTemplate, ), dependencies = dependencies, repositoriesConfiguration = Forma.settings.repositories diff --git a/plugins/android/src/main/java/androidUtil.kt b/plugins/android/src/main/java/androidUtil.kt index 9f2c96e4..7671d58d 100644 --- a/plugins/android/src/main/java/androidUtil.kt +++ b/plugins/android/src/main/java/androidUtil.kt @@ -38,7 +38,9 @@ fun Project.androidUtil( owner: Owner = NoOwner, visibility: Visibility = Public, dependencies: FormaDependency = emptyDependency(), - testDependencies: FormaDependency = emptyDependency() + testDependencies: FormaDependency = emptyDependency(), + /** Enable Jetpack Compose; defaults to project-wide `compose` setting. */ + compose: Boolean = Forma.settings.compose, ) { disallowResources() @@ -47,7 +49,8 @@ fun Project.androidUtil( target.validate(AndroidUtilTargetTemplate) val androidFeatureConfig = AndroidLibraryFeatureConfiguration( - packageName + packageName = packageName, + compose = compose, ) applyFeatures( diff --git a/plugins/android/src/main/java/composeWidget.kt b/plugins/android/src/main/java/composeWidget.kt new file mode 100644 index 00000000..787954ab --- /dev/null +++ b/plugins/android/src/main/java/composeWidget.kt @@ -0,0 +1,70 @@ +import org.gradle.api.Project +import tools.forma.android.feature.AndroidLibraryFeatureConfiguration +import tools.forma.android.feature.androidLibraryFeatureDefinition +import tools.forma.android.feature.applyFeatures +import tools.forma.android.feature.kotlinAndroidFeatureDefinition +import tools.forma.android.target.AndroidUtilTargetTemplate +import tools.forma.android.target.ComposeWidgetTargetTemplate +import tools.forma.android.target.ResourcesTargetTemplate +import tools.forma.android.target.UiLibraryTargetTemplate +import tools.forma.android.target.UtilTargetTemplate +import tools.forma.android.target.WidgetTargetTemplate +import tools.forma.deps.core.FormaDependency +import tools.forma.deps.core.NamedDependency +import tools.forma.deps.core.applyDependencies +import tools.forma.owners.NoOwner +import tools.forma.owners.Owner +import tools.forma.android.visibility.Public +import tools.forma.android.visibility.Visibility +import tools.forma.validation.validate +import tools.forma.validation.validator + +/** + * Compose UI component target (Jetpack Compose counterpart of [widget]). + * + * Always enables Compose (`buildFeatures.compose` + compiler extension). + * May depend on other `compose-widget` / `widget` modules so View and Compose + * UI can coexist (GH #96). + */ +fun Project.composeWidget( + packageName: String, + owner: Owner = NoOwner, + visibility: Visibility = Public, + dependencies: FormaDependency = emptyDependency(), + testDependencies: NamedDependency = emptyDependency(), + androidTestDependencies: NamedDependency = emptyDependency(), + testInstrumentationRunner: String = androidJunitRunner, + consumerMinificationFiles: Set = emptySet(), + manifestPlaceholders: Map = emptyMap() +) { + target.validate(ComposeWidgetTargetTemplate) + + val featureConfiguration = AndroidLibraryFeatureConfiguration( + packageName = packageName, + testInstrumentationRunnerClass = testInstrumentationRunner, + consumerMinificationFiles = consumerMinificationFiles, + manifestPlaceholders = manifestPlaceholders, + compose = true, + selfValidator = validator(ComposeWidgetTargetTemplate) + ) + + applyFeatures( + androidLibraryFeatureDefinition(featureConfiguration), + kotlinAndroidFeatureDefinition() + ) + + applyDependencies( + validator = validator( + UiLibraryTargetTemplate, + ComposeWidgetTargetTemplate, + WidgetTargetTemplate, + UtilTargetTemplate, + AndroidUtilTargetTemplate, + ResourcesTargetTemplate + ), + dependencies = dependencies, + testDependencies = testDependencies, + androidTestDependencies = androidTestDependencies, + repositoriesConfiguration = Forma.settings.repositories + ) +} diff --git a/plugins/android/src/main/java/impl.kt b/plugins/android/src/main/java/impl.kt index 00f1ccfc..014bb20c 100644 --- a/plugins/android/src/main/java/impl.kt +++ b/plugins/android/src/main/java/impl.kt @@ -14,6 +14,7 @@ import tools.forma.android.target.UiLibraryTargetTemplate import tools.forma.android.target.UtilTargetTemplate import tools.forma.android.target.ViewBindingTargetTemplate import tools.forma.android.target.WidgetTargetTemplate +import tools.forma.android.target.ComposeWidgetTargetTemplate import tools.forma.android.utils.BuildConfiguration import tools.forma.deps.core.FormaDependency import tools.forma.deps.core.NamedDependency @@ -22,13 +23,16 @@ import tools.forma.validation.validate import tools.forma.validation.validator /** - * Feature **implementation** (Android library + optional view binding / kapt). + * Feature **implementation** (Android library + optional view binding / kapt / Compose). * * Dagger2-friendly boundaries: * - **May** depend on feature `api` contracts, shared libraries/utils, and UI - * building blocks (`res`, `viewbinding`, `widget`, `ui-library`). + * building blocks (`res`, `viewbinding`, `widget`, `compose-widget`, `ui-library`). * - **Must not** depend on other `impl` modules — feature graphs compose only * at [androidApp] / [androidBinary] so implementations stay independent. + * + * @param compose enable Jetpack Compose for this target; defaults to project-wide + * [androidProjectConfiguration] `compose` flag. */ fun Project.impl( packageName: String, @@ -36,6 +40,7 @@ fun Project.impl( testDependencies: NamedDependency = emptyDependency(), androidTestDependencies: NamedDependency = emptyDependency(), viewBinding: Boolean = false, + compose: Boolean = Forma.settings.compose, testInstrumentationRunner: String = androidJunitRunner, buildConfiguration: BuildConfiguration = BuildConfiguration(), consumerMinificationFiles: Set = emptySet(), @@ -50,7 +55,8 @@ fun Project.impl( consumerMinificationFiles, manifestPlaceholders, selfValidator = validator(ImplTargetTemplate), - viewBinding = viewBinding + viewBinding = viewBinding, + compose = compose ) applyFeatures( androidLibraryFeatureDefinition(libraryFeatureConfiguration), @@ -68,6 +74,7 @@ fun Project.impl( ResourcesTargetTemplate, ViewBindingTargetTemplate, WidgetTargetTemplate, + ComposeWidgetTargetTemplate, ), dependencies = dependencies, testDependencies = testDependencies, diff --git a/plugins/android/src/main/java/tools/forma/android/feature/AndroidBinary.kt b/plugins/android/src/main/java/tools/forma/android/feature/AndroidBinary.kt index 59016dd1..bf5cdb58 100644 --- a/plugins/android/src/main/java/tools/forma/android/feature/AndroidBinary.kt +++ b/plugins/android/src/main/java/tools/forma/android/feature/AndroidBinary.kt @@ -17,6 +17,8 @@ data class AndroidBinaryFeatureConfiguration( val testInstrumentationRunnerClass: String, val consumerMinificationFiles: Set, val manifestPlaceholders: Map = emptyMap(), + /** Enable Jetpack Compose for the application (APK) target. */ + val compose: Boolean = false, val selfValidator: Validator = validator(BinaryTargetTemplate) ) @@ -44,6 +46,12 @@ fun androidBinaryFeatureDefinition( buildTypes.applyFrom(configuration.buildConfiguration) compileOptions.applyFrom(formaConfiguration) + + if (configuration.compose) { + buildFeatures.compose = true + composeOptions.kotlinCompilerExtensionVersion = + formaConfiguration.composeCompilerVersion + } } } ) \ No newline at end of file diff --git a/plugins/android/src/main/java/tools/forma/android/feature/AndroidLibrary.kt b/plugins/android/src/main/java/tools/forma/android/feature/AndroidLibrary.kt index b652e551..c96f47fc 100644 --- a/plugins/android/src/main/java/tools/forma/android/feature/AndroidLibrary.kt +++ b/plugins/android/src/main/java/tools/forma/android/feature/AndroidLibrary.kt @@ -16,6 +16,8 @@ class AndroidLibraryFeatureConfiguration( val consumerMinificationFiles: Set = emptySet(), val manifestPlaceholders: Map = emptyMap(), val viewBinding: Boolean = false, + /** Enable Jetpack Compose for this Android library target. */ + val compose: Boolean = false, val selfValidator: Validator = validator(LibraryTargetTemplate) ) @@ -45,6 +47,11 @@ fun androidLibraryFeatureDefinition( compileOptions.applyFrom(formaConfiguration) buildFeatures.viewBinding = feature.viewBinding + if (feature.compose) { + buildFeatures.compose = true + composeOptions.kotlinCompilerExtensionVersion = + formaConfiguration.composeCompilerVersion + } } } ) diff --git a/plugins/android/src/main/java/tools/forma/android/target/AndroidTargets.kt b/plugins/android/src/main/java/tools/forma/android/target/AndroidTargets.kt index 0a95a5f9..203214c2 100644 --- a/plugins/android/src/main/java/tools/forma/android/target/AndroidTargets.kt +++ b/plugins/android/src/main/java/tools/forma/android/target/AndroidTargets.kt @@ -15,4 +15,6 @@ object ViewBindingTargetTemplate : TargetTemplate("viewbinding") object ResourcesTargetTemplate : TargetTemplate("res") object ApiTargetTemplate : TargetTemplate("api") object ImplTargetTemplate : TargetTemplate("impl") -object WidgetTargetTemplate : TargetTemplate("widget") \ No newline at end of file +object WidgetTargetTemplate : TargetTemplate("widget") +/** Compose UI component target (parallel to [WidgetTargetTemplate] for View system). */ +object ComposeWidgetTargetTemplate : TargetTemplate("compose-widget") \ No newline at end of file diff --git a/plugins/android/src/main/java/uiLibrary.kt b/plugins/android/src/main/java/uiLibrary.kt index e7760c7d..51185a1b 100644 --- a/plugins/android/src/main/java/uiLibrary.kt +++ b/plugins/android/src/main/java/uiLibrary.kt @@ -11,6 +11,7 @@ import tools.forma.android.target.ResourcesTargetTemplate import tools.forma.android.target.UiLibraryTargetTemplate import tools.forma.android.target.UtilTargetTemplate import tools.forma.android.target.WidgetTargetTemplate +import tools.forma.android.target.ComposeWidgetTargetTemplate import tools.forma.android.utils.BuildConfiguration import tools.forma.android.visibility.Public import tools.forma.android.visibility.Visibility @@ -36,6 +37,8 @@ fun Project.uiLibrary( buildConfiguration: BuildConfiguration = BuildConfiguration(), consumerMinificationFiles: Set = emptySet(), manifestPlaceholders: Map = emptyMap(), + /** Enable Jetpack Compose; defaults to project-wide `compose` setting. */ + compose: Boolean = Forma.settings.compose, ): TargetBuilder { target.validate(UiLibraryTargetTemplate) val libraryFeatureConfiguration = AndroidLibraryFeatureConfiguration( @@ -44,6 +47,7 @@ fun Project.uiLibrary( testInstrumentationRunner, consumerMinificationFiles, manifestPlaceholders, + compose = compose, ) applyFeatures( androidLibraryFeatureDefinition(libraryFeatureConfiguration), @@ -54,6 +58,7 @@ fun Project.uiLibrary( validator = validator( // Better to have ability to use widget while we experiment with dependency rules WidgetTargetTemplate, + ComposeWidgetTargetTemplate, UtilTargetTemplate, AndroidUtilTargetTemplate, ResourcesTargetTemplate diff --git a/plugins/android/src/main/java/viewBinding.kt b/plugins/android/src/main/java/viewBinding.kt index 69a22640..d9855a57 100644 --- a/plugins/android/src/main/java/viewBinding.kt +++ b/plugins/android/src/main/java/viewBinding.kt @@ -11,6 +11,7 @@ import tools.forma.android.target.LibraryTargetTemplate import tools.forma.android.target.ResourcesTargetTemplate import tools.forma.android.target.ViewBindingTargetTemplate import tools.forma.android.target.WidgetTargetTemplate +import tools.forma.android.target.ComposeWidgetTargetTemplate import tools.forma.android.validation.onlyAllowLayouts import tools.forma.android.visibility.Public import tools.forma.android.visibility.Visibility @@ -53,6 +54,7 @@ fun Project.viewBinding( validator = validator( ApiTargetTemplate, WidgetTargetTemplate, + ComposeWidgetTargetTemplate, ResourcesTargetTemplate, LibraryTargetTemplate, AndroidUtilTargetTemplate, diff --git a/plugins/android/src/main/java/widget.kt b/plugins/android/src/main/java/widget.kt index 30503dc8..4aeb0074 100644 --- a/plugins/android/src/main/java/widget.kt +++ b/plugins/android/src/main/java/widget.kt @@ -5,6 +5,7 @@ import tools.forma.android.feature.kotlinAndroidFeatureDefinition import tools.forma.android.target.AndroidUtilTargetTemplate import tools.forma.android.target.UtilTargetTemplate import tools.forma.android.target.WidgetTargetTemplate +import tools.forma.android.target.ComposeWidgetTargetTemplate import tools.forma.android.target.ResourcesTargetTemplate import tools.forma.owners.NoOwner import tools.forma.owners.Owner @@ -19,6 +20,12 @@ import tools.forma.deps.core.NamedDependency import tools.forma.validation.validate // TODO only allow layouts and view classes +/** + * Custom View / UI component target. + * + * Compose UI lives in [composeWidget] (separate suffix) so View and Compose + * graphs stay distinguishable while still allowed to depend on each other. + */ fun Project.widget( packageName: String, owner: Owner = NoOwner, @@ -48,6 +55,7 @@ fun Project.widget( validator = validator( UiLibraryTargetTemplate, WidgetTargetTemplate, + ComposeWidgetTargetTemplate, UtilTargetTemplate, AndroidUtilTargetTemplate, ResourcesTargetTemplate diff --git a/plugins/config/src/main/java/tools/forma/config/AndroidProjectSettings.kt b/plugins/config/src/main/java/tools/forma/config/AndroidProjectSettings.kt index aedee579..1db1bf5d 100644 --- a/plugins/config/src/main/java/tools/forma/config/AndroidProjectSettings.kt +++ b/plugins/config/src/main/java/tools/forma/config/AndroidProjectSettings.kt @@ -28,7 +28,22 @@ data class AndroidProjectSettings( val kotlinVersion: String, val agpVersion: String, val repositories: RepositoryHandler.() -> Unit, + /** + * Project-wide default for per-target `compose` flags + * (`impl`, `androidLibrary`, `androidUtil`, `androidApp`, `uiLibrary`, + * `androidBinary`). Individual targets may still pass `compose = true/false` + * to override. Does **not** auto-enable `composeWidget` modules (those always + * enable Compose themselves). + */ val compose: Boolean, + /** + * Jetpack Compose compiler extension version applied when a target enables + * Compose (`composeOptions.kotlinCompilerExtensionVersion`). Must match the + * Kotlin version used by the project (see Compose Compiler compatibility map). + * Default `1.5.3` pairs with Kotlin **1.9.10** (Gradle 8.4 embedded Kotlin used + * by the sample application). Override when bumping Kotlin. + */ + val composeCompilerVersion: String, val vectorDrawablesUseSupportLibrary: Boolean, val javaVersionCompatibility: JavaVersion, // Java/Kotlin configuration val mandatoryOwners: Boolean diff --git a/scripts/env-mac.sh b/scripts/env-mac.sh index 2c0c45dc..0d8c5dc3 100755 --- a/scripts/env-mac.sh +++ b/scripts/env-mac.sh @@ -9,7 +9,8 @@ # export ANDROID_HOME="/usr/local/share/android-commandlinetools" # yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --licenses # "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --install \ -# "platforms;android-33" "platform-tools" "build-tools;33.0.2" "build-tools;34.0.0" +# "platforms;android-34" "platforms;android-33" "platform-tools" \ +# "build-tools;33.0.2" "build-tools;34.0.0" # printf 'sdk.dir=%s\n' "$ANDROID_HOME" > application/local.properties # # Note: brew install --cask temurin@17 needs sudo for the system pkg installer; @@ -25,6 +26,9 @@ if [[ ! -x "$JAVA_HOME/bin/java" ]]; then return 1 2>/dev/null || exit 1 fi +if [[ ! -d "$ANDROID_HOME/platforms/android-34" ]]; then + echo "env-mac.sh: warning: Android platform 34 missing under $ANDROID_HOME (sample compileSdk)" >&2 +fi if [[ ! -d "$ANDROID_HOME/platforms/android-33" ]]; then echo "env-mac.sh: warning: Android platform 33 missing under $ANDROID_HOME" >&2 fi