
Generates type-safe form controllers from annotated data classes, delivering state management, validation, auto-rendered fields with focus/keyboard wiring, configurable styling, and UI integration.
Formidable generates type-safe form controllers from annotated Kotlin data classes. Define your form once — get state management, validation, and Compose integration on Android, iOS, and Web.
Three steps. That's it.
@FormSchema
data class LoginForm(
@Field(label = "Email")
@Email
val email: String = "",
@Field(label = "Password")
@MinLength(8)
val password: String = "",
)KSP generates LoginFormController — no boilerplate, no reflection.
class LoginViewModel : ViewModel() {
val controller = LoginFormController()
}@Composable
fun LoginScreen(viewModel: LoginViewModel) {
val emailState by viewModel.controller.email.collectAsState()
val passwordState by viewModel.controller.password.collectAsState()
val isValid by viewModel.controller.isValid.collectAsState()
Formidable {
StringField(
state = emailState,
onValueChange = { viewModel.controller.updateEmail(it) },
onFocusLost = { viewModel.controller.touchEmail() },
)
StringField(
state = passwordState,
onValueChange = { viewModel.controller.updatePassword(it) },
onFocusLost = { viewModel.controller.touchPassword() },
config = {
visualTransformation = PasswordVisualTransformation()
keyboardType = KeyboardType.Password
},
)
Button(onClick = { /* submit */ }, enabled = isValid) {
Text("Login")
}
}
}Formidable renders OutlinedTextField for you, auto-wires focus, keyboard navigation, and error display. Need a different style or a password field? Use config = { ... }. Need full control? Pass a trailing lambda instead.
// build.gradle.kts
plugins {
id("com.google.devtools.ksp") version "2.2.20-2.0.3"
}
dependencies {
implementation("io.github.wassimbeltaief:formidable-core:2.1.7")
implementation("io.github.wassimbeltaief:formidable-compose:2.1.7")
ksp("io.github.wassimbeltaief:formidable-ksp:2.1.7")
}KSP in KMP runs against commonMain metadata, which requires a small amount of extra wiring to make the generated sources visible to all targets.
// build.gradle.kts
plugins {
id("com.google.devtools.ksp") version "2.2.20-2.0.3"
}
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.wassimbeltaief:formidable-core:2.1.7")
implementation("io.github.wassimbeltaief:formidable-compose:2.1.7")
}
}
}
dependencies {
// KSP processes @FormSchema in commonMain, generates a Controller for all targets
add("kspCommonMainMetadata", "io.github.wassimbeltaief:formidable-ksp:2.1.7")
}
// Ensure KSP metadata runs before any compilation task
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask<*>>().configureEach {
if (!name.startsWith("ksp")) {
dependsOn("kspCommonMainKotlinMetadata")
}
}
// Make generated sources visible in commonMain
kotlin.sourceSets.commonMain {
kotlin.srcDir(layout.buildDirectory.dir("generated/ksp/metadata/commonMain/kotlin"))
}wassimbeltaief.github.io/Formidable — interactive demo running in the browser
| In-Depth Guide | Schema → controller, rendering modes, FieldScope, all field types with examples, validation reference |
| GitHub Pages | Interactive docs + live demo |
| Sample App | Full sign-up form: async validation, cross-field rules, conditional fields, enum dropdown |
| Changelog | Release history |
| Contributing | How to contribute |
| Item | Description |
|---|---|
| Localization / i18n | Structured ValidationError sealed classes instead of hardcoded strings, enabling full control over error message localization via stringResource() or any i18n system |
formidable-persistence |
SavedStateHandle + retain {} strategies for surviving process death |
formidable-wizard |
Multi-step forms: @Step, WizardState, FormWizard {}
|
formidable-autofill |
Compose 1.8 autofill semantics injection via @AutofillType
|
formidable-showcase |
Live WASM playground on GitHub Pages |
| Password style |
FieldStyle.Text.Password that bundles PasswordVisualTransformation and an eye-icon toggle |
| Date / time picker |
DateField and TimeField types with platform-native pickers |
| Character counter | Auto-render shows remaining characters when @MaxLength is set |
| Theming | Global default styles for auto-render fields (override OutlinedTextField defaults once, apply everywhere) |
| Compose Desktop target | Extend CMP support beyond Android + iOS + WASM |
| Form serialization |
controller.toJson() / controller.fromJson() for draft saving |
| Server error DSL |
controller.setErrors(mapOf("username" to "Already taken")) batch API |
Contributions are welcome! Please read the contributing guidelines before submitting a pull request.
MIT — Copyright © 2024-present Wassim Beltaief
Formidable generates type-safe form controllers from annotated Kotlin data classes. Define your form once — get state management, validation, and Compose integration on Android, iOS, and Web.
Three steps. That's it.
@FormSchema
data class LoginForm(
@Field(label = "Email")
@Email
val email: String = "",
@Field(label = "Password")
@MinLength(8)
val password: String = "",
)KSP generates LoginFormController — no boilerplate, no reflection.
class LoginViewModel : ViewModel() {
val controller = LoginFormController()
}@Composable
fun LoginScreen(viewModel: LoginViewModel) {
val emailState by viewModel.controller.email.collectAsState()
val passwordState by viewModel.controller.password.collectAsState()
val isValid by viewModel.controller.isValid.collectAsState()
Formidable {
StringField(
state = emailState,
onValueChange = { viewModel.controller.updateEmail(it) },
onFocusLost = { viewModel.controller.touchEmail() },
)
StringField(
state = passwordState,
onValueChange = { viewModel.controller.updatePassword(it) },
onFocusLost = { viewModel.controller.touchPassword() },
config = {
visualTransformation = PasswordVisualTransformation()
keyboardType = KeyboardType.Password
},
)
Button(onClick = { /* submit */ }, enabled = isValid) {
Text("Login")
}
}
}Formidable renders OutlinedTextField for you, auto-wires focus, keyboard navigation, and error display. Need a different style or a password field? Use config = { ... }. Need full control? Pass a trailing lambda instead.
// build.gradle.kts
plugins {
id("com.google.devtools.ksp") version "2.2.20-2.0.3"
}
dependencies {
implementation("io.github.wassimbeltaief:formidable-core:2.1.7")
implementation("io.github.wassimbeltaief:formidable-compose:2.1.7")
ksp("io.github.wassimbeltaief:formidable-ksp:2.1.7")
}KSP in KMP runs against commonMain metadata, which requires a small amount of extra wiring to make the generated sources visible to all targets.
// build.gradle.kts
plugins {
id("com.google.devtools.ksp") version "2.2.20-2.0.3"
}
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.wassimbeltaief:formidable-core:2.1.7")
implementation("io.github.wassimbeltaief:formidable-compose:2.1.7")
}
}
}
dependencies {
// KSP processes @FormSchema in commonMain, generates a Controller for all targets
add("kspCommonMainMetadata", "io.github.wassimbeltaief:formidable-ksp:2.1.7")
}
// Ensure KSP metadata runs before any compilation task
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask<*>>().configureEach {
if (!name.startsWith("ksp")) {
dependsOn("kspCommonMainKotlinMetadata")
}
}
// Make generated sources visible in commonMain
kotlin.sourceSets.commonMain {
kotlin.srcDir(layout.buildDirectory.dir("generated/ksp/metadata/commonMain/kotlin"))
}wassimbeltaief.github.io/Formidable — interactive demo running in the browser
| In-Depth Guide | Schema → controller, rendering modes, FieldScope, all field types with examples, validation reference |
| GitHub Pages | Interactive docs + live demo |
| Sample App | Full sign-up form: async validation, cross-field rules, conditional fields, enum dropdown |
| Changelog | Release history |
| Contributing | How to contribute |
| Item | Description |
|---|---|
| Localization / i18n | Structured ValidationError sealed classes instead of hardcoded strings, enabling full control over error message localization via stringResource() or any i18n system |
formidable-persistence |
SavedStateHandle + retain {} strategies for surviving process death |
formidable-wizard |
Multi-step forms: @Step, WizardState, FormWizard {}
|
formidable-autofill |
Compose 1.8 autofill semantics injection via @AutofillType
|
formidable-showcase |
Live WASM playground on GitHub Pages |
| Password style |
FieldStyle.Text.Password that bundles PasswordVisualTransformation and an eye-icon toggle |
| Date / time picker |
DateField and TimeField types with platform-native pickers |
| Character counter | Auto-render shows remaining characters when @MaxLength is set |
| Theming | Global default styles for auto-render fields (override OutlinedTextField defaults once, apply everywhere) |
| Compose Desktop target | Extend CMP support beyond Android + iOS + WASM |
| Form serialization |
controller.toJson() / controller.fromJson() for draft saving |
| Server error DSL |
controller.setErrors(mapOf("username" to "Already taken")) batch API |
Contributions are welcome! Please read the contributing guidelines before submitting a pull request.
MIT — Copyright © 2024-present Wassim Beltaief