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
35 changes: 20 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,40 +99,45 @@ The translation is mechanical — `new` instead of bare calls, commas instead of
| `count++` | `count++` (operator on `MutableNumberState<T>`) |
| `"Count: $count"` | `$"Count: {count}"` (via `MutableState<T>.ToString`) |

That's the entire [`MainActivity.cs`](src/ComposeNet.Sample/MainActivity.cs) — ~27 lines including ceremony, 13 for the composition itself.
That's an end-to-end Material 3 counter app in ~13 lines of composition — start from this shape when adding a new screen. The actual [`src/ComposeNet.Sample/MainActivity.cs`](src/ComposeNet.Sample/MainActivity.cs) in the repo is a much larger **kitchen-sink demo** that exercises every facade in a tabbed `Scaffold`; for a single-screen real-app example see [`samples/Jetchat`](samples/Jetchat).

## What's wrapped today

The facade [`ComposeNet.Compose`](src/ComposeNet.Compose) covers the common Material 3 + Foundation surface:

| Category | Composables |
| ----------------------- | ----------- |
| Theme & layout | `MaterialTheme`, `Column`, `Row`, `Box`, `Spacer`, `Scaffold`, `HorizontalDivider`, `VerticalDivider` |
| Lazy lists | `LazyColumn<T>`, `LazyRow<T>`, `LazyVerticalGrid<T>`, `LazyHorizontalGrid<T>` (+ `GridCells`) |
| Theme & layout | `MaterialTheme` (parameterizable `ColorScheme`/`Typography`/`Shapes`/`Dark`/`UseDynamicColor`, plus `CurrentColorScheme`/`CurrentTypography`/`CurrentShapes` reads), `Column`, `Row` (`Arrangement`), `Box`, `Spacer`, `Scaffold`, `HorizontalDivider`, `VerticalDivider`, `BoxWithConstraints` |
| Lazy lists & paging | `LazyColumn<T>`, `LazyRow<T>`, `LazyVerticalGrid<T>`, `LazyHorizontalGrid<T>`, `LazyVerticalStaggeredGrid<T>`, `LazyHorizontalStaggeredGrid<T>` (+ `GridCells`/`StaggeredGridCells`), `HorizontalPager`, `VerticalPager` (+ `PagerState`), `FlowRow`, `FlowColumn` |
| Carousels & pull | `HorizontalMultiBrowseCarousel`, `HorizontalCenteredHeroCarousel`, `HorizontalUncontainedCarousel`, `PullToRefreshBox` (+ `PullToRefreshState`) |
| Surfaces | `Surface`, `Card`, `ElevatedCard`, `OutlinedCard` |
| App bars | `TopAppBar` family (Center/Medium/Large/Flexible), `BottomAppBar`, `FlexibleBottomAppBar` |
| App bars | `TopAppBar` family (Center/Medium/Large/Flexible — with optional subtitles via Phase 9 branching), `BottomAppBar`, `FlexibleBottomAppBar` |
| Tabs | `TabRow` family (Primary/Secondary, scrollable variants), `Tab`, `LeadingIconTab`, `CustomTab` |
| Buttons | `Button`, `IconButton`, `FloatingActionButton` |
| Text & input | `Text`, `TextField`, `OutlinedTextField` |
| Media | `Image`, `Icon` |
| Buttons | `Button`, `OutlinedButton`, `TextButton`, `ElevatedButton`, `FilledTonalButton`, `IconButton`, `OutlinedIconButton`, `FilledIconButton`, `FilledTonalIconButton`, full `IconToggleButton` family, `FloatingActionButton` (+ `Small`/`Large`/`Extended` variants) |
| Text & input | `Text` (`TextStyle`/`FontWeight`/`FontStyle`/`FontFamily`/`TextDecoration`/`TextAlign`/`TextOverflow`), `TextField`, `OutlinedTextField`, `SecureTextField`, `OutlinedSecureTextField` |
| Media | `Image`, `Icon` (drawable-resource and `ImageVector` overloads) |
| Chips | `AssistChip`, `FilterChip`, `InputChip`, `SuggestionChip` (+ `Elevated*` variants) |
| Selection | `Checkbox`, `TriStateCheckbox`, `RadioButton`, `Switch`, `Slider`, `RangeSlider`, `SegmentedButton` |
| Selection | `Checkbox`, `TriStateCheckbox`, `RadioButton`, `Switch`, `Slider`, `RangeSlider`, `SegmentedButton`, `SingleChoiceSegmentedButtonRow`, `MultiChoiceSegmentedButtonRow` |
| Progress & feedback | `CircularProgressIndicator`, `LinearProgressIndicator`, `ListItem`, `Badge`, `BadgedBox` |
| Menus & search | `DropdownMenu` + `DropdownMenuItem`, `SearchBar` family (Top, ExpandedDocked, ExpandedFullScreen) |
| Navigation | `NavigationBar`, `NavigationRail`, `WideNavigationRail`, `ModalWideNavigationRail` (+ items) |
| Drawers | `ModalNavigationDrawer`, `DismissibleNavigationDrawer`, `PermanentNavigationDrawer` |
| Sheets & pickers | `ModalBottomSheet`, `BottomSheetScaffold`, `DatePicker`/`DatePickerDialog`, `TimePicker`/`TimePickerDialog` |
| Menus & search | `DropdownMenu` + `DropdownMenuItem`, `ExposedDropdownMenuBox` + `ExposedDropdownMenu`, `SearchBar` family (Top, ExpandedDocked, ExpandedFullScreen, `DockedSearchBar`) |
| Navigation | `NavHost`, `NavController`, `NavBackStackEntry`, `NavigationBar`, `NavigationRail`, `WideNavigationRail`, `ModalWideNavigationRail` (+ items) |
| Drawers | `ModalNavigationDrawer`, `DismissibleNavigationDrawer`, `PermanentNavigationDrawer` (+ matching sheets, generated via Phase 10 `[ConfirmStateChange]`) |
| Sheets & pickers | `ModalBottomSheet`, `BottomSheetScaffold`, `DatePicker`/`DatePickerDialog`, `DateRangePicker`/`DateRangePickerDialog`, `TimePicker`/`TimeInput`/`TimePickerDialog` |
| Overlays | `AlertDialog`, `Snackbar` + `SnackbarHost`, `Tooltip` |
| Modifier chains | `Padding`, `FillMaxWidth/Height/Size`, `Width`, `Height`, `Size`, `SafeDrawingPadding`, `SystemBarsPadding` |
| State | `Remember` (+ keyed `Remember(factory, key1, …)`, `RememberKeyed`), `RememberSaveable` (+ keyed), `MutableState<T>`, `MutableNumberState<T>`, `MutableStateList<T>`, `MutableStateMap<K,V>`, `DerivedStateOf`, `ProduceState`, plus `DatePickerState`, `TimePickerState`, `SearchBarState`, `SnackbarHostState` |
| Animation | `AnimatedVisibility`, `AnimatedContent`, `Crossfade` |
| Effects | `Compose.LaunchedEffect`, `Compose.DisposableEffect`, `Compose.SideEffect` |
| Modifier chains | `Padding`, `FillMaxWidth/Height/Size`, `Width`, `Height`, `Size`, `AspectRatio`, `Offset`, `Alpha`, `Background`, `Border`, `Clip`, `Clickable`, `Weight`, `VerticalScroll`/`HorizontalScroll` (+ `ScrollState`), `Draggable` (+ `DraggableState`), focus/semantics/gestures, `SafeDrawingPadding`, `SystemBarsPadding` |
| Value types | `Color` (+ `FromRgb`/`FromArgb`/`FromHex` and theme reads), `Dp`, `Sp`, `FontWeight`, `TextAlign`, `Shape` |
| State | `Remember` (+ keyed `Remember(factory, key1, …)`, `RememberKeyed`), `RememberSaveable` (+ keyed), `MutableState<T>`, `MutableNumberState<T>`, `MutableStateList<T>`, `MutableStateMap<K,V>`, `DerivedStateOf`, `ProduceState`, plus `DatePickerState`, `DateRangePickerState`, `TimePickerState`, `SearchBarState`, `SnackbarHostState`, `ScrollState`, `PagerState`, `PullToRefreshState`, `DraggableState`, `DrawerStateHolder`, `WideNavigationRailState`, `FocusRequester`/`FocusState` |
| Async | `SuspendBridge` — Kotlin `suspend` functions surfaced as C# `Task` / `Task<T>` (drives `ScrollState.ScrollToAsync`, `LazyListState.AnimateScrollToItemAsync`, `SnackbarHostState.ShowSnackbarAsync`, etc.) |

## Samples

[`samples/`](samples) mirrors the official [`android/compose-samples`](https://github.com/android/compose-samples) repo in C#. See [`samples/README.md`](samples/README.md) for the scoreboard of which samples are ported and what was simplified.

## Status

The sample builds, deploys to an Android 16 (API 36) emulator, and renders a real Material 3 UI end-to-end: dynamic Material You colors, edge-to-edge layout, an interactive `Button` that increments `MutableNumberState<int>` and recomposes the count.
The sample builds, deploys to an Android 16 (API 36) emulator, and renders a real Material 3 UI end-to-end: dynamic Material You colors via parameterizable `MaterialTheme`, edge-to-edge layout, an interactive `Button` that increments `MutableNumberState<int>` and recomposes the count. The kitchen-sink demo in [`src/ComposeNet.Sample`](src/ComposeNet.Sample) exercises the full facade across a tabbed `Scaffold` (text styling, lists, pickers, dialogs, sheets, navigation, animation, effects, search, dropdowns, draggable modifiers, …).

The facade and sample reference the official `Xamarin.AndroidX.Compose.*` 1.11.2.x and `Xamarin.AndroidX.Compose.Material3` 1.4.0.x NuGets directly — the per-binding projects this repo originally needed have been deleted.

Expand Down
106 changes: 98 additions & 8 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,14 @@ boilerplate (cached `IntPtr` class/method handles, signature constants,
`try { Call… } finally { GC.KeepAlive(…) }` around every managed
wrapper whose `.Handle` was read into a `JValue`, and `DeleteLocalRef`
for local string refs) is emitted by `ComposeBridgeGenerator` in
[`ComposeNet.SourceGenerators`](../src/ComposeNet.SourceGenerators). Only
two outliers stay hand-written: `ModifierHandle` (a managed
[`ComposeNet.SourceGenerators`](../src/ComposeNet.SourceGenerators). For
the simplest facade shapes, the entire `Render` body itself is also
generated by `ComposeFacadeGenerator` from a one-line `[ComposeFacade]`
attribute stacked on the bridge — see
[`.github/copilot-instructions.md`](../.github/copilot-instructions.md)
("Facade generator — `[ComposeFacade]`") for the ~10 phases of facade
shapes the generator covers. Only two outliers stay hand-written at
the JNI layer: `ModifierHandle` (a managed
`IModifier? → IntPtr` conversion that none of the bridge shapes fit)
and `ModifierCompanionInstance` (a `$$INSTANCE` static field lookup,
not a method invocation). The user never sees any of this; when
Expand Down Expand Up @@ -59,9 +65,30 @@ default." Hand-writing those bitmasks at every call site is illegible
(`_changed: 0b0111`); writing the `[Flags]` enum by hand is tedious
and bit-rots when the Kotlin signature changes.

[`ComposeNet.SourceGenerators`](../src/ComposeNet.SourceGenerators) is a
small Roslyn incremental generator triggered by an assembly-level
attribute. It supports two forms.
[`ComposeNet.SourceGenerators`](../src/ComposeNet.SourceGenerators) hosts
three Roslyn incremental generators that together eliminate the
boilerplate:

- **`ComposeDefaultsGenerator`** — emits a `[Flags] enum` per
composable, one bit per Kotlin parameter, driven by
`[ComposeDefaults]` (described below). All declarations live in
[`ComposeDefaults.cs`](../src/ComposeNet.Compose/ComposeDefaults.cs).
- **`ComposeBridgeGenerator`** — emits the JNI body of each bridge
partial method declared in
[`ComposeBridges.cs`](../src/ComposeNet.Compose/ComposeBridges.cs)
from a single `[ComposeBridge(Class=…, JvmName=…, Signature=…)]`
attribute.
- **`ComposeFacadeGenerator`** — emits the full
`ComposableNode`-derived facade class (ctor + `Render` body) for
bridges whose shape it recognizes, driven by `[ComposeFacade]`
stacked on the bridge declaration. See
[`.github/copilot-instructions.md`](../.github/copilot-instructions.md)
for the ~10 facade-shape "phases" (containers, callbacks, named
slots, painter resources, state holders with parameterised
Remember, default-from-theme color slots, bridge branching, JCW
confirm-state-change adapters, …).

The `$default` generator supports two forms.

**Generic form** — when the binder exposes the Kt method:

Expand Down Expand Up @@ -102,6 +129,26 @@ all of these declarations — every composable in the facade gets its
pin the emitted output. When the upstream binder fix lands, each
declarative attribute can be swapped one-for-one to the generic form.

## Compose value types

The Kotlin `@JvmInline value class` types that surface as primitives
across JNI (`Color`, `Dp`, `Sp`, `FontWeight`, `TextAlign`) are
mirrored as typed C# structs in
[`ComposeNet.Compose`](../src/ComposeNet.Compose). The bridge generator
keeps a tiny registry in
[`ComposeValueTypes.cs`](../src/ComposeNet.SourceGenerators/ComposeValueTypes.cs)
that maps each value type to its JNI slot (`F` / `J` / `I`) and a
`Pack(T?)` helper, so a bridge can declare `Dp? width` or `Sp? size`
and the generator emits the correct primitive lowering plus the
`$default`-bit clear on non-null. `Color` is a 64-bit packed ULong with
an implicit conversion to `long`, so call sites pass `Color.FromRgb(…)`
or one of the named constants straight through to bridges and bound
binding methods that already take a packed color — no per-call
conversion required. Reference-typed wrappers (`FontWeight`,
`TextDecoration`, `Shape`) go through the generic
"reference-type → handle with null check" path the bridge generator
already supports.

## What's missing on the C# side (and why)

| Kotlin | C# today | Cost |
Expand Down Expand Up @@ -164,6 +211,15 @@ class.
`Modifier` class via a one-time JNI fetch of the `$$INSTANCE` field
— invisible to callers. See [NOTES.md](NOTES.md) open issue #1 for
the upstream-friendly fix.
- **Theming reads landed (#61).** Use
`MaterialTheme.CurrentColorScheme(composer)`,
`CurrentTypography(composer)`, and `CurrentShapes(composer)` from
inside `Render` to read the active theme; mirror of Kotlin's
`MaterialTheme.colorScheme / typography / shapes` reads.
`MaterialTheme` itself takes `ColorScheme`/`Typography`/`Shapes`/`Dark`/`UseDynamicColor`
as settable properties — see `MaterialTheme.LightColorScheme()` /
`DarkColorScheme()` / `DynamicLightColorScheme(...)` /
`DynamicDarkColorScheme(...)` factories.
- **`remember(keys, …)` is supported.** Use the keyed overloads
`Remember(factory, key1)`, `Remember(factory, key1, key2)`,
`Remember(factory, key1, key2, key3)`, or
Expand All @@ -181,6 +237,40 @@ class.
available. `ProduceState` is implemented purely in C# via an
`IRememberObserver` JCW — the producer is a plain
`Func<MutableState<T>, CancellationToken, Task>`, not a Kotlin
suspend lambda. Tracked in #62. Still missing:
`snapshotFlow { … }` (Flow → `IAsyncEnumerable` bridge) and custom
`Saver<T, S>` (only `autoSaver` is exposed today).
suspend lambda. Still missing: `snapshotFlow { … }`
(Flow → `IAsyncEnumerable` bridge) and custom `Saver<T, S>` (only
`autoSaver` is exposed today).
- **Effects.** `Compose.LaunchedEffect`, `Compose.DisposableEffect`,
and `Compose.SideEffect` are bound (#57 / #128). `LaunchedEffect`
takes a plain `Func<CancellationToken, Task>` and a key list
(rather than a Kotlin suspend lambda); cancellation happens on key
change / leaving composition just like Kotlin.
- **Suspend functions.** `SuspendBridge` (PR #97) lets a
hand-written bridge return Kotlin's `COROUTINE_SUSPENDED` sentinel
and complete a `Task<T>` from the eventual resume. Used by
`ScrollState.ScrollToAsync` / `LazyListState.AnimateScrollToItemAsync`
/ `SnackbarHostState.ShowSnackbarAsync`. The drawer family
(`ModalNavigationDrawer` etc.) exposes a `DrawerStateHolder`
wrapper but programmatic `open()` / `close()` suspend bridges are
not yet wired up.
- **Compose Navigation.** `NavHost` / `NavController` /
`NavBackStackEntry` are bound (#60). Pass route lambdas via
`NavGraphBuilderLambda`; deep links are not yet exposed.

## Still missing (tracked)

- `CompositionLocal` / `CompositionLocalProvider` — see
[#59](https://github.com/jonathanpeppers/compose-net/issues/59).
- Drawing primitives: `Canvas`, `Modifier.drawBehind`, `Brush`,
`Path`, `Shape` factories — see
[#64](https://github.com/jonathanpeppers/compose-net/issues/64).
- WindowInsets padding modifiers (`imePadding`,
`navigationBarsPadding`, `statusBarsPadding`, …) — see
[#69](https://github.com/jonathanpeppers/compose-net/issues/69).
- M3 Expressive newcomers (SplitButton, ButtonGroup,
LoadingIndicator, FAB menus) — see
[#54](https://github.com/jonathanpeppers/compose-net/issues/54) /
[#103](https://github.com/jonathanpeppers/compose-net/issues/103).
- Programmatic drawer / bottom-sheet open via `suspend` — the JCW
veto-adapter pattern is in place (Phase 10 `[ConfirmStateChange]`)
but the suspend-bridge `Open()`/`Close()` aren't surfaced yet.
Loading
Loading