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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 15 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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 |
Expand All @@ -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 <a href="https://www.flaticon.com/authors/freepik" title="Freepik">Freepik</a>
from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a>
2 changes: 1 addition & 1 deletion TICKETS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
3 changes: 3 additions & 0 deletions application/binary/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),

Expand All @@ -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")
)
Expand Down
3 changes: 2 additions & 1 deletion application/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
6 changes: 6 additions & 0 deletions application/common/greeting/compose-widget/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
composeWidget(
packageName = "tools.forma.sample.common.greeting.compose.widget",
dependencies = deps(
androidx.compose
)
)
Original file line number Diff line number Diff line change
@@ -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)
)
}
}
16 changes: 16 additions & 0 deletions build-dependencies/dependencies/src/main/kotlin/Androidx.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
)
}
3 changes: 3 additions & 0 deletions build-dependencies/dependencies/src/main/kotlin/Versions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
25 changes: 14 additions & 11 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand All @@ -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:
Expand All @@ -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

Expand Down Expand Up @@ -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 ]
)
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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 |
Expand Down
108 changes: 108 additions & 0 deletions docs/COMPOSE.md
Original file line number Diff line number Diff line change
@@ -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 = <composeCompilerVersion>`

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`.
Loading
Loading