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