ketoy-dev 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +17 -0
- package/README.md +101 -0
- package/SECURITY.md +34 -0
- package/dist/ketoy.js +3165 -0
- package/dist/ketoy.js.map +1 -0
- package/package.json +78 -0
- package/skills/ketoy/README.md +50 -0
- package/skills/ketoy/SKILL.md +148 -0
- package/skills/ketoy/examples/capabilities-stubs.kt +60 -0
- package/skills/ketoy/examples/hilt-config.kt +192 -0
- package/skills/ketoy/examples/no-hilt-config.kt +101 -0
- package/skills/ketoy/examples/todo-screen.kt +156 -0
- package/skills/ketoy/guides/build-and-analyze.md +87 -0
- package/skills/ketoy/guides/diagnose-errors.md +129 -0
- package/skills/ketoy/guides/init-project.md +127 -0
- package/skills/ketoy/guides/migrate.md +190 -0
- package/skills/ketoy/guides/publish-deferred.md +46 -0
- package/skills/ketoy/guides/safe-edits.md +141 -0
- package/skills/ketoy/reference/architecture-cheatsheet.md +122 -0
- package/skills/ketoy/reference/capabilities.md +122 -0
- package/skills/ketoy/reference/forbidden-apis.md +149 -0
- package/skills/ketoy/reference/supported-composables.md +80 -0
- package/skills/ketoy/reference/supported-constructors.md +57 -0
- package/skills/ketoy/reference/supported-modifiers.md +76 -0
- package/skills/ketoy/templates/app-build.gradle.kts.tmpl +109 -0
- package/skills/ketoy/templates/ketoy-capabilities.json.tmpl +21 -0
- package/skills/ketoy/templates/manifest-snippet.xml.tmpl +33 -0
- package/templates/HelloKetoyScreen.kt.tmpl +51 -0
- package/templates/MainActivity.kt.tmpl +53 -0
- package/templates/MyApplication.kt.tmpl +88 -0
- package/templates/ketoy-capabilities.json.tmpl +5 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Forbidden APIs in KBC Source
|
|
2
|
+
|
|
3
|
+
KBC bytecode is **sandboxed**. The compiler plugin rejects entire categories of calls. This is the source of truth for "what can't I do" — every entry here corresponds to a typed diagnostic in `KetoyCompilationError`.
|
|
4
|
+
|
|
5
|
+
## 1. Direct Android / AndroidX APIs
|
|
6
|
+
|
|
7
|
+
**Banned**: any FQ starting with `android.` or `androidx.` **outside the registered Compose surface**.
|
|
8
|
+
|
|
9
|
+
```kotlin
|
|
10
|
+
// ❌ KetoyBC: Direct access to android.widget.Toast.makeText
|
|
11
|
+
Toast.makeText(context, "hi", Toast.LENGTH_SHORT).show()
|
|
12
|
+
|
|
13
|
+
// ❌ KetoyBC: Direct access to android.content.Context.getSystemService
|
|
14
|
+
val vm = context.getSystemService(VIBRATOR_SERVICE)
|
|
15
|
+
|
|
16
|
+
// ✅ via capability
|
|
17
|
+
toast("hi") // stub at 0x0901
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The diagnostic includes a fix example pointing at the equivalent capability.
|
|
21
|
+
|
|
22
|
+
## 2. Reflection
|
|
23
|
+
|
|
24
|
+
**Banned**: `kotlin.reflect.*`, `java.lang.Class.get*`, `java.lang.reflect.*`.
|
|
25
|
+
|
|
26
|
+
```kotlin
|
|
27
|
+
// ❌ KetoyBC: Reflection is not allowed in KBC source
|
|
28
|
+
val klass = MyClass::class
|
|
29
|
+
val type = typeOf<MyClass>()
|
|
30
|
+
val fields = obj.javaClass.declaredFields
|
|
31
|
+
|
|
32
|
+
// ✅ Use sealed classes + when() exhaustive checks
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
KBC has no class metadata at runtime — reflection literally cannot work. Use sealed classes or enums for type-discriminated logic.
|
|
36
|
+
|
|
37
|
+
## 3. File I/O
|
|
38
|
+
|
|
39
|
+
**Banned**: `java.io.*`, `java.nio.*`, `kotlin.io.*` (which wraps `java.io`).
|
|
40
|
+
|
|
41
|
+
```kotlin
|
|
42
|
+
// ❌ KetoyBC: File I/O is not allowed in KBC source
|
|
43
|
+
File("config.json").readText()
|
|
44
|
+
FileInputStream(path).use { ... }
|
|
45
|
+
|
|
46
|
+
// ✅ via storage capability
|
|
47
|
+
val raw = kvGet("config_json") as String?
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
For persistent state use `KV_GET/SET/OBSERVE` (DataStore-backed in the standard library) or `DB_QUERY/OBSERVE` (Room-backed via `KBCRoomBridge`).
|
|
51
|
+
|
|
52
|
+
## 4. Unstructured concurrency
|
|
53
|
+
|
|
54
|
+
**Banned**: `GlobalScope`, `runBlocking`, `kotlinx.coroutines.CoroutineScope(...)` factory.
|
|
55
|
+
|
|
56
|
+
```kotlin
|
|
57
|
+
// ❌ KetoyBC: GlobalScope is forbidden — use viewModelScope
|
|
58
|
+
GlobalScope.launch { fetchData() }
|
|
59
|
+
|
|
60
|
+
// ❌ KetoyBC: runBlocking is forbidden
|
|
61
|
+
val result = runBlocking { suspendFn() }
|
|
62
|
+
|
|
63
|
+
// ✅ inside a @KetoyComposable, use LaunchedEffect
|
|
64
|
+
LaunchedEffect(key) {
|
|
65
|
+
val result = fetchData() // a @KetoyCapabilityStub suspend fn
|
|
66
|
+
vmSetState("result", result)
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Allowed: `viewModelScope.launch`, `coroutineScope { ... }` builder (structured), `LaunchedEffect`, `withContext(Dispatchers.IO)` (via `WITH_CONTEXT` opcode + dispatcher capabilities).
|
|
71
|
+
|
|
72
|
+
## 5. Direct construction of non-allowed types
|
|
73
|
+
|
|
74
|
+
If a constructor isn't in `supported-constructors.md`, the compiler plugin throws `KetoyBC: Direct construction of <type> in KBC source`. The most common culprits:
|
|
75
|
+
|
|
76
|
+
```kotlin
|
|
77
|
+
// ❌ Brush gradients not on catalog
|
|
78
|
+
Brush.linearGradient(listOf(Color.Red, Color.Blue))
|
|
79
|
+
|
|
80
|
+
// ❌ Arbitrary data classes (try to construct UiState(...) inside a Composable)
|
|
81
|
+
data class UiState(val name: String)
|
|
82
|
+
UiState("hi")
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Fix: pass the value in via state (`vmGetState`) or define an adapter.
|
|
86
|
+
|
|
87
|
+
## 6. `@Composable` without `@KetoyComposable`
|
|
88
|
+
|
|
89
|
+
```kotlin
|
|
90
|
+
// ❌ KetoyBC: Unregistered composable com.example.MyHelper
|
|
91
|
+
@Composable
|
|
92
|
+
fun MyHelper() { Text("hi") }
|
|
93
|
+
|
|
94
|
+
@KetoyComposable
|
|
95
|
+
@Composable
|
|
96
|
+
fun Root() {
|
|
97
|
+
MyHelper() // crashes here
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Why**: bare `@Composable` is treated as an external call that needs an adapter (which doesn't exist for arbitrary user code). Either:
|
|
102
|
+
- Add `@KetoyComposable` to the helper (it becomes a KBC function), OR
|
|
103
|
+
- Inline the helper into the calling screen.
|
|
104
|
+
|
|
105
|
+
This is intentional — Phase 9A tightened detection so `MyHelper` doesn't silently become uncallable.
|
|
106
|
+
|
|
107
|
+
## 7. Transitive closure errors
|
|
108
|
+
|
|
109
|
+
Closure validation walks every helper reachable from a `@KetoyComposable` / `@KetoyEntryPoint` / `@KetoyViewModel` root. If a helper does something forbidden, the error carries a breadcrumb:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
KetoyBC: Direct access to android.util.Log.d in helper
|
|
113
|
+
Reached via: TodoListScreen → renderHeader → logHeaderShown
|
|
114
|
+
...
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Fix at the leaf**, not the root. The breadcrumb points you there.
|
|
118
|
+
|
|
119
|
+
**Important**: same-module top-level functions that are NEVER called from a KBC root are **left alone** — they compile to plain DEX. The closure walk only validates code reachable from a KBC root. So you can have native helpers and KBC helpers side-by-side in `:app`.
|
|
120
|
+
|
|
121
|
+
## 8. Lambda captures (closure conversion)
|
|
122
|
+
|
|
123
|
+
KBC supports closure conversion for `val`/captured reads, BUT:
|
|
124
|
+
- Captures are snapshotted **eagerly** at the call site. Mutating the parent `val` later does not affect the lambda.
|
|
125
|
+
- Only outer-scope `val`s and parameter reads work. `var` writes from inside a lambda don't propagate.
|
|
126
|
+
- Deeply nested lambdas with many captures slow down compilation. Keep capture sets small.
|
|
127
|
+
|
|
128
|
+
```kotlin
|
|
129
|
+
// ✅ works — captured outer val
|
|
130
|
+
@KetoyComposable
|
|
131
|
+
fun Screen() {
|
|
132
|
+
val name = "World"
|
|
133
|
+
Column {
|
|
134
|
+
Text("Hello, $name!") // name captured into the Column content lambda
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ❌ won't update the outer var
|
|
139
|
+
fun Screen() {
|
|
140
|
+
var count = 0
|
|
141
|
+
Button(onClick = { count++ }) { Text("$count") } // count writes lost
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ✅ use state instead
|
|
145
|
+
fun Screen() {
|
|
146
|
+
val count = vmGetState("count") as Int? ?: 0
|
|
147
|
+
Button(onClick = { vmSetState("count", count + 1) }) { Text("$count") }
|
|
148
|
+
}
|
|
149
|
+
```
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Supported Composables (as of 0.2.1-alpha)
|
|
2
|
+
|
|
3
|
+
Every composable callable from KBC source must have a registered adapter. The canonical list is in `ketoy-adapters-material3/src/main/resources/adapter-scan-roots.txt`. Below is the human-readable summary. App-specific adapters live at `0x4000+` registered through a host scan-roots file.
|
|
4
|
+
|
|
5
|
+
If a composable is **not** in this list, KBC compilation will reject the call with `KetoyBC: Unregistered composable <FQ>`. The fix is one of: (a) it's supported under a different overload — disambiguate; (b) add a custom adapter via `KBCComposableAdapter.register(...)`; (c) keep the screen native.
|
|
6
|
+
|
|
7
|
+
## Layout & containers
|
|
8
|
+
|
|
9
|
+
| FQ | Adapter ID | Notes |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| `androidx.compose.material3.Text` | `0x0001` | String overload, 17 params (post-1.5). AnnotatedString form not supported. |
|
|
12
|
+
| `androidx.compose.foundation.layout.Column` | `0x0002` | |
|
|
13
|
+
| `androidx.compose.foundation.layout.Row` | `0x0003` | |
|
|
14
|
+
| `androidx.compose.foundation.layout.Box` | `0x0004` | |
|
|
15
|
+
| `androidx.compose.foundation.layout.Spacer` | `0x000B` | |
|
|
16
|
+
| `androidx.compose.material3.Scaffold` | `0x0007` | `topBar`, `bottomBar`, `floatingActionButton`, `snackbarHost` content slots all supported. |
|
|
17
|
+
| `androidx.compose.material3.Surface` | `0x0008` | Non-clickable overload (modifier-first). Clickable overload not yet supported. |
|
|
18
|
+
| `androidx.compose.material3.Card` | `0x0009` | 6-param shape/colors/elevation/border form. |
|
|
19
|
+
|
|
20
|
+
## Lazy lists — NOT supported
|
|
21
|
+
|
|
22
|
+
`LazyColumn`, `LazyRow` are explicitly deferred (KNOWN_ISSUES #5/#6). The content slot is `LazyListScope.() -> Unit`, a receiver-scoped composable that the KSP codegen doesn't yet handle. **Workaround**: use `Column` with `verticalArrangement` for small lists; for large lists keep the screen native and dispatch through navigation.
|
|
23
|
+
|
|
24
|
+
## Buttons & interaction
|
|
25
|
+
|
|
26
|
+
| FQ | Adapter ID | Notes |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `androidx.compose.material3.Button` | `0x000C` | |
|
|
29
|
+
| `androidx.compose.material3.IconButton` | `0x000F` | |
|
|
30
|
+
| `androidx.compose.material3.Checkbox` | `0x0014` | |
|
|
31
|
+
| `androidx.compose.material3.Switch` | `0x0016` | |
|
|
32
|
+
|
|
33
|
+
## Text input
|
|
34
|
+
|
|
35
|
+
| FQ | Adapter ID | Notes |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `androidx.compose.material3.TextField` | `0x0012` | String overload only. AnnotatedString and TextFieldValue not supported. |
|
|
38
|
+
|
|
39
|
+
## Media
|
|
40
|
+
|
|
41
|
+
| FQ | Adapter ID | Notes |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| `androidx.compose.foundation.Image` | `0x0024` | Use `painterResource(R.drawable.X)`; host must register `X` in `MaterialDrawableResolver`. |
|
|
44
|
+
| `coil.compose.AsyncImage` | (see scan-roots) | The painters-and-callbacks overload. |
|
|
45
|
+
| `androidx.compose.material3.Icon` | `0x0019` | Use `Icons.<Style>.<Name>`; host must register the icon in `MaterialIconsResolver`. Material icons-extended required on classpath. |
|
|
46
|
+
|
|
47
|
+
## TopAppBar family
|
|
48
|
+
|
|
49
|
+
| FQ | Adapter ID |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `androidx.compose.material3.TopAppBar` | (see scan-roots) |
|
|
52
|
+
|
|
53
|
+
Note: `TopAppBarDefaults.topAppBarColors(...)` is **constructed** via `CONSTRUCT_JVM`, not called as a composable. See `supported-constructors.md`.
|
|
54
|
+
|
|
55
|
+
## How to verify a composable is supported
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
grep -i 'YourComposable' /path/to/ketoyvm/ketoy-adapters-material3/src/main/resources/adapter-scan-roots.txt
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
If grep returns nothing → not supported. Don't invent.
|
|
62
|
+
|
|
63
|
+
## Adding a custom adapter (app-specific)
|
|
64
|
+
|
|
65
|
+
Custom composables live in your app's KBC scope at `0x4000+`. Pattern:
|
|
66
|
+
|
|
67
|
+
```kotlin
|
|
68
|
+
// In your app's Hilt module or App.onCreate:
|
|
69
|
+
KBCComposableAdapter.register(
|
|
70
|
+
id = 0x4001,
|
|
71
|
+
fqName = "com.yourapp.ui.MyChart",
|
|
72
|
+
) { p ->
|
|
73
|
+
MyChart(
|
|
74
|
+
data = p.getAnyOrNull(0) as List<Float>? ?: emptyList(),
|
|
75
|
+
modifier = p.getModifier(1),
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Then declare a matching `@KetoyCapabilityStub`-like function in KBC source — or use a custom adapter scan-roots file feeding KSP. See `docs/adapters/custom-composable-adapters.md` in the Ketoy repo for the full flow.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Supported Constructors (CONSTRUCT_JVM)
|
|
2
|
+
|
|
3
|
+
When KBC source writes `TextStyle(fontSize = 16.sp, color = Color.White)` or `RoundedCornerShape(40.dp)`, the compiler plugin emits a `CONSTRUCT_JVM` opcode targeting a registered constructor adapter. Only the constructors listed here are supported.
|
|
4
|
+
|
|
5
|
+
If your KBC code constructs an object whose constructor isn't on this list, you'll get `KetoyBC: Direct construction of <FQ> in KBC source`. The fix is either: (a) use a supported alternative; (b) add a custom `KBCConstructorAdapter` in your host; (c) move the construction host-side and pass via state.
|
|
6
|
+
|
|
7
|
+
## Built-in constructors
|
|
8
|
+
|
|
9
|
+
| FQ / Factory | Notes |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `androidx.compose.ui.text.TextStyle` | The full kitchen-sink TextStyle ctor. |
|
|
12
|
+
| `androidx.compose.foundation.text.KeyboardOptions` | |
|
|
13
|
+
| `androidx.compose.foundation.text.KeyboardActions` | |
|
|
14
|
+
| `androidx.compose.foundation.shape.RoundedCornerShape` | Both `(Dp)` and `(Int percent)` forms. |
|
|
15
|
+
| `androidx.compose.ui.text.input.PasswordVisualTransformation` | |
|
|
16
|
+
| `androidx.compose.material3.ButtonDefaults.buttonColors` (factory) | Pass as named args; supported via top-level factory pattern. |
|
|
17
|
+
| `androidx.compose.material3.CardDefaults.cardColors` (factory) | |
|
|
18
|
+
| `androidx.compose.material3.TopAppBarDefaults.topAppBarColors` (factory) | |
|
|
19
|
+
| `androidx.compose.ui.graphics.Color(Long)` | The `Color(0xFFAARRGGBB)` literal form. |
|
|
20
|
+
|
|
21
|
+
## Atomic-collapse encoder shortcuts
|
|
22
|
+
|
|
23
|
+
The compiler plugin recognizes these patterns and rewrites them inline — they look like constructor calls in source but don't emit `CONSTRUCT_JVM`:
|
|
24
|
+
|
|
25
|
+
| Source pattern | Wire encoding | Host resolution |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| `Color.Red`, `Color.White`, etc. (12 named) | `KBCValue.ColorARGB` literal | inline |
|
|
28
|
+
| `Color(0xFFAARRGGBB)` | `KBCValue.ColorARGB` literal | inline |
|
|
29
|
+
| `16.dp`, `8.dp`, etc. | `KBCValue.Dp(Float)` | inline |
|
|
30
|
+
| `12.sp` | `KBCValue.Sp(Float)` | inline |
|
|
31
|
+
| `FontWeight.Bold`, `.Medium`, etc. (9) | `KBCValue.FontWeightInt` | inline |
|
|
32
|
+
| `FontStyle.Italic` / `.Normal` | `KBCValue.FontStyleId` | inline |
|
|
33
|
+
| `TextAlign.Center` etc. (6) | `KBCValue.TextAlignId` | inline |
|
|
34
|
+
| `TextOverflow.Ellipsis` etc. | `KBCValue.TextOverflowId` | inline |
|
|
35
|
+
| `Arrangement.Center` etc. (12) | `KBCValue.HorizontalArrangementId` / `VerticalArrangementId` | inline |
|
|
36
|
+
| `Alignment.Center` etc. (15) | `KBCValue.HorizontalAlignmentId` / `VerticalAlignmentId` / `ContentAlignmentId` | inline |
|
|
37
|
+
| `ContentScale.Fit` etc. (7) | `KBCValue.ContentScaleId` | inline |
|
|
38
|
+
| `KeyboardType.Password` etc. (8) | `KBCValue.KeyboardTypeId` | inline |
|
|
39
|
+
| `ImeAction.Done` etc. (8) | `KBCValue.ImeActionId` | inline |
|
|
40
|
+
| `FontFamily.Default` / `.SansSerif` / `.Serif` / `.Monospace` / `.Cursive` | `KBCValue.FontFamilyTokenId` | inline |
|
|
41
|
+
| `FontFamily(Font(R.font.X))` | `KBCValue.StringLiteral("X")` | Host `MaterialFontFamilyResolver` |
|
|
42
|
+
| `painterResource(R.drawable.X)` | `KBCValue.StringLiteral("X")` | Host `MaterialDrawableResolver` |
|
|
43
|
+
| `Icons.<Style>.<Name>` | `KBCValue.StringLiteral("Style.Name")` | Host `MaterialIconsResolver` |
|
|
44
|
+
|
|
45
|
+
## NOT supported
|
|
46
|
+
|
|
47
|
+
- Arbitrary data class construction (`MyConfig(...)` — define a `KBCComposableAdapter` parameter instead, or pass via state).
|
|
48
|
+
- `Modifier.then(otherModifier)` from outside the supported builder set.
|
|
49
|
+
- `Brush.linearGradient(...)`, `Brush.verticalGradient(...)` — gradient brushes not on the catalog yet.
|
|
50
|
+
- Custom `Shape` implementations.
|
|
51
|
+
- `AnnotatedString.Builder` and `buildAnnotatedString { }`.
|
|
52
|
+
|
|
53
|
+
## Verifying a constructor is supported
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
grep -i 'YourType' /path/to/ketoyvm/ketoy-adapters-material3/src/main/resources/adapter-scan-roots.txt | grep '^constructor='
|
|
57
|
+
```
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Supported Modifiers
|
|
2
|
+
|
|
3
|
+
The `KBCModifierIRWalker` recognizes a fixed set of `Modifier.*` builder calls and encodes them as `KBCModifierOp` entries in the bundle's modifier table. Anything outside this list either passes through as a no-op (rare) or breaks the build with `Unregistered composable <Modifier helper FQ>`.
|
|
4
|
+
|
|
5
|
+
## Layout
|
|
6
|
+
|
|
7
|
+
| Builder | Notes |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `Modifier.fillMaxSize()` / `fillMaxSize(fraction)` | |
|
|
10
|
+
| `Modifier.fillMaxWidth()` / `fillMaxWidth(fraction)` | |
|
|
11
|
+
| `Modifier.fillMaxHeight()` / `fillMaxHeight(fraction)` | |
|
|
12
|
+
| `Modifier.size(Dp)` / `size(width = ..., height = ...)` | |
|
|
13
|
+
| `Modifier.width(Dp)` | |
|
|
14
|
+
| `Modifier.height(Dp)` | |
|
|
15
|
+
| `Modifier.padding(Dp)` | All sides |
|
|
16
|
+
| `Modifier.padding(horizontal = ..., vertical = ...)` | |
|
|
17
|
+
| `Modifier.padding(start = ..., top = ..., end = ..., bottom = ...)` | Partial defaults supported (e.g. `padding(top = 24.dp)`) |
|
|
18
|
+
| `Modifier.padding(paddingValues = pv)` | PaddingValues from `Scaffold` content slot — register-passthrough |
|
|
19
|
+
| `Modifier.weight(Float)` | Inside `Row`/`Column` only; layout-scope handled |
|
|
20
|
+
| `Modifier.offset(x = Dp, y = Dp)` | |
|
|
21
|
+
|
|
22
|
+
## Appearance
|
|
23
|
+
|
|
24
|
+
| Builder | Notes |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `Modifier.background(color = Color)` | Color must be supported token / `Color(0xFFAARRGGBB)` |
|
|
27
|
+
| `Modifier.background(brush = ...)` | NOT supported (Brush construction outside catalog) |
|
|
28
|
+
| `Modifier.border(width = Dp, color = Color)` | |
|
|
29
|
+
| `Modifier.clip(shape = ...)` | Shape must be `RoundedCornerShape(...)` |
|
|
30
|
+
| `Modifier.alpha(Float)` | |
|
|
31
|
+
| `Modifier.zIndex(Float)` | |
|
|
32
|
+
| `Modifier.aspectRatio(Float)` | |
|
|
33
|
+
|
|
34
|
+
## Interaction
|
|
35
|
+
|
|
36
|
+
| Builder | Notes |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `Modifier.clickable { ... }` | Lambda dispatches via VM. No `indication` / `interactionSource` overload. |
|
|
39
|
+
| `Modifier.testTag(String)` | |
|
|
40
|
+
|
|
41
|
+
## NOT supported
|
|
42
|
+
|
|
43
|
+
- `Modifier.then(otherModifier)` from outside the catalog.
|
|
44
|
+
- `Modifier.semantics { ... }`.
|
|
45
|
+
- `Modifier.layoutId(...)`, `ConstraintLayout` modifiers.
|
|
46
|
+
- `Modifier.shadow(...)` with elevation Dp — limited support; verify with a tiny test.
|
|
47
|
+
- `Modifier.draw*` modifiers (`drawBehind`, `drawWithContent`).
|
|
48
|
+
- `Modifier.pointerInput(...)` / gesture detectors.
|
|
49
|
+
- Custom `LayoutModifier` implementations.
|
|
50
|
+
|
|
51
|
+
## Patterns
|
|
52
|
+
|
|
53
|
+
Chained modifiers with mixed token kinds:
|
|
54
|
+
|
|
55
|
+
```kotlin
|
|
56
|
+
Modifier
|
|
57
|
+
.fillMaxWidth()
|
|
58
|
+
.padding(horizontal = 20.dp, vertical = 12.dp)
|
|
59
|
+
.clip(shape = RoundedCornerShape(12.dp))
|
|
60
|
+
.background(color = Color(0xFFEEEEFF))
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Partial-default named-arg padding (this works since Phase D of ADR-0001):
|
|
64
|
+
|
|
65
|
+
```kotlin
|
|
66
|
+
Modifier.padding(top = 48.dp, start = 16.dp, end = 16.dp) // bottom defaults to 0.dp
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Layout-scope `weight`:
|
|
70
|
+
|
|
71
|
+
```kotlin
|
|
72
|
+
Row(modifier = Modifier.fillMaxWidth()) {
|
|
73
|
+
Card(modifier = Modifier.weight(3f)) { ... }
|
|
74
|
+
Card(modifier = Modifier.weight(2f)) { ... }
|
|
75
|
+
}
|
|
76
|
+
```
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// REFERENCE SHAPE — NOT A TARGET TO WRITE WHOLESALE.
|
|
2
|
+
//
|
|
3
|
+
// This is the *final shape* of an `app/build.gradle.kts` after Ketoy
|
|
4
|
+
// integration. Use it as a guide for surgical edits via the safe-edits
|
|
5
|
+
// strategy (skills/ketoy/guides/safe-edits.md). If the user has an
|
|
6
|
+
// existing `app/build.gradle.kts` — and they almost always do — you
|
|
7
|
+
// MUST edit it surgically: insert the plugin, append the dependencies,
|
|
8
|
+
// append the `ketoy { }` block. Overwriting will destroy their existing
|
|
9
|
+
// dependencies, signing config, build types, flavors, ProGuard rules,
|
|
10
|
+
// Crashlytics integration, custom tasks, etc.
|
|
11
|
+
//
|
|
12
|
+
// Only write this file verbatim if the project does not yet have an app
|
|
13
|
+
// module (e.g. fresh `gradle init` skeleton). Even then, you're better
|
|
14
|
+
// off generating the module via Android Studio and editing it surgically.
|
|
15
|
+
//
|
|
16
|
+
// {{PROJECT_NAME}} — placeholders: {{NAMESPACE}}, {{APPLICATION_ID}},
|
|
17
|
+
// {{MIN_SDK}} (>=26), {{TARGET_SDK}}, {{COMPILE_SDK}}, {{VERSION_CODE}},
|
|
18
|
+
// {{VERSION_NAME}}
|
|
19
|
+
|
|
20
|
+
plugins {
|
|
21
|
+
alias(libs.plugins.android.application)
|
|
22
|
+
alias(libs.plugins.kotlin.android)
|
|
23
|
+
alias(libs.plugins.kotlin.compose)
|
|
24
|
+
// Hilt — keep only if the project uses Hilt:
|
|
25
|
+
// alias(libs.plugins.ksp)
|
|
26
|
+
// alias(libs.plugins.hilt)
|
|
27
|
+
id("dev.ketoy.compiler")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
android {
|
|
31
|
+
namespace = "{{NAMESPACE}}"
|
|
32
|
+
compileSdk = {{COMPILE_SDK}}
|
|
33
|
+
|
|
34
|
+
defaultConfig {
|
|
35
|
+
applicationId = "{{APPLICATION_ID}}"
|
|
36
|
+
minSdk = {{MIN_SDK}} // Ketoy requires API 26+
|
|
37
|
+
targetSdk = {{TARGET_SDK}}
|
|
38
|
+
versionCode = {{VERSION_CODE}}
|
|
39
|
+
versionName = "{{VERSION_NAME}}"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
compileOptions {
|
|
43
|
+
sourceCompatibility = JavaVersion.VERSION_17
|
|
44
|
+
targetCompatibility = JavaVersion.VERSION_17
|
|
45
|
+
}
|
|
46
|
+
kotlinOptions {
|
|
47
|
+
jvmTarget = "17"
|
|
48
|
+
}
|
|
49
|
+
buildFeatures {
|
|
50
|
+
compose = true
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
dependencies {
|
|
55
|
+
// ─── Ketoy ─────────────────────────────────────────────────
|
|
56
|
+
implementation(platform("dev.ketoy.vm:ketoy-bom:0.2.1-alpha"))
|
|
57
|
+
implementation("dev.ketoy.vm:ketoy-runtime")
|
|
58
|
+
implementation("dev.ketoy.vm:ketoy-capabilities-core")
|
|
59
|
+
implementation("dev.ketoy.vm:ketoy-capabilities-navigation")
|
|
60
|
+
implementation("dev.ketoy.vm:ketoy-adapters-material3")
|
|
61
|
+
implementation("dev.ketoy.vm:ketoy-annotations")
|
|
62
|
+
// For Hilt projects, also:
|
|
63
|
+
// implementation("dev.ketoy.vm:ketoy-hilt")
|
|
64
|
+
testImplementation("dev.ketoy.vm:ketoy-test")
|
|
65
|
+
|
|
66
|
+
// ─── AndroidX + Compose ──────────────────────────────────────
|
|
67
|
+
implementation(libs.androidx.core.ktx)
|
|
68
|
+
implementation(libs.androidx.lifecycle.runtime.ktx)
|
|
69
|
+
implementation(libs.androidx.activity.compose)
|
|
70
|
+
implementation(platform(libs.androidx.compose.bom))
|
|
71
|
+
implementation(libs.androidx.compose.ui)
|
|
72
|
+
implementation(libs.androidx.compose.material3)
|
|
73
|
+
// Required if your KBC screens reference Icons.<Style>.<Name>:
|
|
74
|
+
implementation(libs.androidx.compose.material.icons.extended)
|
|
75
|
+
implementation(libs.androidx.navigation.compose)
|
|
76
|
+
|
|
77
|
+
// ─── DataStore (used by storage capabilities) ───────────────
|
|
78
|
+
implementation(libs.androidx.datastore.preferences)
|
|
79
|
+
|
|
80
|
+
// ─── Coroutines ──────────────────────────────────────────────
|
|
81
|
+
implementation(libs.kotlinx.coroutines.android)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─── KetoyBC compiler plugin configuration ──────────────────────
|
|
85
|
+
//
|
|
86
|
+
// `exportFromAppModule = true` makes the compiler plugin attach to
|
|
87
|
+
// compileReleaseKotlin and emit KBC bytecode only for the transitive
|
|
88
|
+
// call closure rooted in `@KetoyComposable` / `@KetoyEntryPoint` /
|
|
89
|
+
// `@KetoyViewModel` declarations in THIS module. Native code in the
|
|
90
|
+
// same module compiles to DEX as plain Kotlin, untouched.
|
|
91
|
+
//
|
|
92
|
+
// Bundle lands at: app/src/main/assets/ketoy/main.ktx
|
|
93
|
+
ketoy {
|
|
94
|
+
exportFromAppModule.set(true)
|
|
95
|
+
bundleId.set("main")
|
|
96
|
+
bundleVariant.set("release")
|
|
97
|
+
capabilityRegistryFile.set(file("ketoy-capabilities.json"))
|
|
98
|
+
debugMode.set(true)
|
|
99
|
+
|
|
100
|
+
// Ed25519 raw 32-byte seed for signing. Generate via:
|
|
101
|
+
// openssl genpkey -algorithm Ed25519 -outform DER -out key.der
|
|
102
|
+
// tail -c 32 key.der > app/keys/sample-private.key
|
|
103
|
+
// Matching public key at app/src/main/assets/ketoy/keys/sample-public.key.
|
|
104
|
+
// The private key is .gitignored.
|
|
105
|
+
val signingKey = file("keys/sample-private.key")
|
|
106
|
+
if (signingKey.exists()) {
|
|
107
|
+
signingKeyFile.set(signingKey)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"allowedStdlibFqNames": [],
|
|
4
|
+
"capabilities": [
|
|
5
|
+
{ "id": 1792, "name": "NAV_PUSH",
|
|
6
|
+
"fqName": "{{PACKAGE}}.ketoyscreens.navPush",
|
|
7
|
+
"kind": "SYNC", "required": true },
|
|
8
|
+
{ "id": 1793, "name": "NAV_POP",
|
|
9
|
+
"fqName": "{{PACKAGE}}.ketoyscreens.navPop",
|
|
10
|
+
"kind": "SYNC", "required": true },
|
|
11
|
+
{ "id": 2560, "name": "VM_GET_STATE",
|
|
12
|
+
"fqName": "{{PACKAGE}}.ketoyscreens.vmGetState",
|
|
13
|
+
"kind": "SYNC", "required": true },
|
|
14
|
+
{ "id": 2561, "name": "VM_SET_STATE",
|
|
15
|
+
"fqName": "{{PACKAGE}}.ketoyscreens.vmSetState",
|
|
16
|
+
"kind": "SYNC", "required": true },
|
|
17
|
+
{ "id": 2562, "name": "VM_OBSERVE_STATE",
|
|
18
|
+
"fqName": "{{PACKAGE}}.ketoyscreens.vmObserveState",
|
|
19
|
+
"kind": "FLOW", "required": true }
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SURGICAL EDIT ONLY: add the `android:name` attribute to the user's EXISTING
|
|
3
|
+
<application> tag. Do NOT replace the whole <application> block — it
|
|
4
|
+
carries android:icon, android:theme, android:label, android:allowBackup,
|
|
5
|
+
possibly tools:replace, network security config, FileProviders, etc.
|
|
6
|
+
|
|
7
|
+
If the existing <application> already has android:name pointing at a
|
|
8
|
+
non-Ketoy class, stop and ask whether to extend that class or repoint.
|
|
9
|
+
|
|
10
|
+
For Hilt projects, use the Hilt-aware Application class.
|
|
11
|
+
|
|
12
|
+
Non-Hilt:
|
|
13
|
+
-->
|
|
14
|
+
<application
|
|
15
|
+
android:name=".App"
|
|
16
|
+
android:allowBackup="true"
|
|
17
|
+
android:icon="@mipmap/ic_launcher"
|
|
18
|
+
android:label="@string/app_name"
|
|
19
|
+
android:theme="@style/Theme.{{APP_NAME}}">
|
|
20
|
+
<!-- ...activities... -->
|
|
21
|
+
</application>
|
|
22
|
+
|
|
23
|
+
<!--
|
|
24
|
+
Hilt:
|
|
25
|
+
-->
|
|
26
|
+
<application
|
|
27
|
+
android:name=".MyApplication"
|
|
28
|
+
android:allowBackup="true"
|
|
29
|
+
android:icon="@mipmap/ic_launcher"
|
|
30
|
+
android:label="@string/app_name"
|
|
31
|
+
android:theme="@style/Theme.{{APP_NAME}}">
|
|
32
|
+
<!-- ...activities... -->
|
|
33
|
+
</application>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
package {{PACKAGE}}
|
|
2
|
+
|
|
3
|
+
import androidx.compose.foundation.layout.Arrangement
|
|
4
|
+
import androidx.compose.foundation.layout.Column
|
|
5
|
+
import androidx.compose.foundation.layout.Spacer
|
|
6
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
7
|
+
import androidx.compose.foundation.layout.height
|
|
8
|
+
import androidx.compose.foundation.layout.padding
|
|
9
|
+
import androidx.compose.material3.Text
|
|
10
|
+
import androidx.compose.runtime.Composable
|
|
11
|
+
import androidx.compose.ui.Alignment
|
|
12
|
+
import androidx.compose.ui.Modifier
|
|
13
|
+
import androidx.compose.ui.graphics.Color
|
|
14
|
+
import androidx.compose.ui.text.font.FontWeight
|
|
15
|
+
import androidx.compose.ui.unit.dp
|
|
16
|
+
import androidx.compose.ui.unit.sp
|
|
17
|
+
import dev.ketoy.annotations.KetoyComposable
|
|
18
|
+
import dev.ketoy.annotations.KetoyEntryPoint
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Default KBC entry-point shipped by `ketoy init`.
|
|
22
|
+
*
|
|
23
|
+
* The KetoyBC compiler plugin compiles this function (and every other
|
|
24
|
+
* `@KetoyComposable` / `@KetoyEntryPoint` reachable from it) into the
|
|
25
|
+
* `main.ktx` bundle written to `assets/ketoy/main.ktx` at release build
|
|
26
|
+
* time. At runtime, `KetoyScreen(entryPoint = "HelloKetoyScreen", ...)`
|
|
27
|
+
* loads + executes it.
|
|
28
|
+
*/
|
|
29
|
+
@KetoyEntryPoint
|
|
30
|
+
@KetoyComposable
|
|
31
|
+
@Composable
|
|
32
|
+
fun HelloKetoyScreen(modifier: Modifier = Modifier) {
|
|
33
|
+
Column(
|
|
34
|
+
modifier = Modifier.fillMaxSize().padding(24.dp),
|
|
35
|
+
verticalArrangement = Arrangement.Center,
|
|
36
|
+
horizontalAlignment = Alignment.CenterHorizontally,
|
|
37
|
+
) {
|
|
38
|
+
Text(
|
|
39
|
+
text = "Hello, Ketoy!",
|
|
40
|
+
fontSize = 28.sp,
|
|
41
|
+
fontWeight = FontWeight.Bold,
|
|
42
|
+
color = Color(0xFF4F378B),
|
|
43
|
+
)
|
|
44
|
+
Spacer(Modifier.height(8.dp))
|
|
45
|
+
Text(
|
|
46
|
+
text = "This screen ships as KBC bytecode inside the APK.",
|
|
47
|
+
fontSize = 14.sp,
|
|
48
|
+
color = Color.DarkGray,
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
package {{PACKAGE}}
|
|
2
|
+
|
|
3
|
+
import android.os.Bundle
|
|
4
|
+
import androidx.activity.ComponentActivity
|
|
5
|
+
import androidx.activity.compose.setContent
|
|
6
|
+
import androidx.activity.enableEdgeToEdge
|
|
7
|
+
import androidx.compose.foundation.layout.Arrangement
|
|
8
|
+
import androidx.compose.foundation.layout.Column
|
|
9
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
10
|
+
import androidx.compose.material3.MaterialTheme
|
|
11
|
+
import androidx.compose.material3.Surface
|
|
12
|
+
import androidx.compose.material3.Text
|
|
13
|
+
import androidx.compose.runtime.CompositionLocalProvider
|
|
14
|
+
import androidx.compose.ui.Alignment
|
|
15
|
+
import androidx.compose.ui.Modifier
|
|
16
|
+
import dev.ketoy.runtime.bundle.KetoyBundleSource
|
|
17
|
+
import dev.ketoy.runtime.compose.KetoyScreen
|
|
18
|
+
import dev.ketoy.runtime.compose.LocalKetoyBundleLoader
|
|
19
|
+
import dev.ketoy.runtime.compose.LocalKetoyRuntime
|
|
20
|
+
|
|
21
|
+
class MainActivity : ComponentActivity() {
|
|
22
|
+
|
|
23
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
24
|
+
super.onCreate(savedInstanceState)
|
|
25
|
+
enableEdgeToEdge()
|
|
26
|
+
val app = application as MyApplication
|
|
27
|
+
setContent {
|
|
28
|
+
MaterialTheme {
|
|
29
|
+
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
|
30
|
+
CompositionLocalProvider(
|
|
31
|
+
LocalKetoyRuntime provides app.ketoyRuntime,
|
|
32
|
+
LocalKetoyBundleLoader provides app.ketoyBundleLoader,
|
|
33
|
+
) {
|
|
34
|
+
KetoyScreen(
|
|
35
|
+
entryPoint = "HelloKetoyScreen",
|
|
36
|
+
bundleSource = KetoyBundleSource.Asset("ketoy/main.ktx"),
|
|
37
|
+
) {
|
|
38
|
+
// Native fallback — rendered when the .ktx bundle is absent,
|
|
39
|
+
// incompatible, or corrupt. Replace with your own native screen.
|
|
40
|
+
Column(
|
|
41
|
+
modifier = Modifier.fillMaxSize(),
|
|
42
|
+
verticalArrangement = Arrangement.Center,
|
|
43
|
+
horizontalAlignment = Alignment.CenterHorizontally,
|
|
44
|
+
) {
|
|
45
|
+
Text("Hello Android")
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|