Home
Form Conductor
A declarative form validation library for Kotlin.
Form conductor is more than form validation. It provides a handful of reusable API to construct a form in simple easy steps. Form conductor tries to tackle three aspects of forms:
Form Data Handling
Form State Management
Form Validation
Form conductor is now published on Maven Central
as me.naingaungluu.formconductor
.
Check Installation Docs for details
🔨 Form construction using built-in annotations​
@Form
data class SignUpForm(
@MinLength(2)
val name: String = "",
@IntegerRange(min = 18, max = 99)
val age: Int = 0,
@EmailAddress
val emailAddress: String = "",
val gender: Gender = Gender.Male,
@Optional
@MaxLength(150)
val address: String? = null
@IsChecked
val termsAndConditionAgreed: Boolean = false
@MaxLength(200)
val bio: String = ""
)
Using Jetpack Compose​
@Composable
fun FormScreen() {
Column {
form(SignUpForm::class) {
/**
* Following properties are available
* formState - State<FormResult<SignUpForm>>
* registerField() - returns field object
*/
Button(
text = "Sign Up",
enabled = this.formState.value is FormResult.Success
)
}
}
}
form(SignUpForm::class) {
field(SignUpForm::name) {
/**
* Following properties are available
* state - compose state with field value: State<FieldValue<String>>
* resultState - validation result state: State<FieldResult<String>>
* setField() - sets the field value and validate
*/
TextField(
value = state.value?.value.orEmpty(),
onValueChange = this::setField,
isError = resultState.value is FieldResult.Error
)
}
}
Full Example​
@Composable
fun FormScreen() {
Column {
form(SignUpForm::class) {
field(SignUpFormData::name) {
TextField(
value = state.value?.value.orEmpty(),
onValueChange = this::setField,
isError = resultState.value is FieldResult.Error
)
}
field(SignUpFormData::emailAddress) {
TextField(
value = state.value?.value.orEmpty(),
onValueChange = this::setField
)
}
field(SignUpFormData::gender) {
Row(Modifier.selectableGroup()) {
RadioButton(
selected = state.value?.value == Gender.Male,
onClick = { setField(Gender.Male) },
modifier = Modifier.semantics { contentDescription = "Male" }
)
RadioButton(
selected = state.value?.value == Gender.Female,
onClick = { setField(Gender.Female) },
modifier = Modifier.semantics { contentDescription = "Male" }
)
}
}
}
}
}
Using Traditional Form Building (Android and JVM apps)​
@Form
data class LoginForm(
@EmailAddress
val emailAddress: String = "",
@MinLength(8)
val password: String = ""
)
Declarative approach​
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Declarative Form Building
val formState = form(LoginForm::class) {
field(LoginForm::emailAddress) {
etEmailAddress.doAfterTextChanged {
this.setField(it)
}
this.resultStream.collectLatest {
when(it) {
is FieldResult.Error -> {
/**
* Available properties in Error
* message - internal error message : String
* failedRule - ValidationRule<String, EmailAddress>
*
* You can compose your error message as needed
*/
etEmailAddress.error = it.message
}
}
}
}
}
}
Imperative Approach​
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Imperative Form Building
val formState = form(LoginForm::class)
val emailAddressState = form.field(LoginForm::emailAddress)
val passwordState = form.field(LoginForm::password)
etLogin.doAfterTextChanged {
emailAddressState.setField(it)
}
etPassword.doAfterTextChanged {
passwordState.setField(it)
}
emailAddresState.resultStream.collectLatest {
if (it is FieldResult.Error) {
etEmailAddress.error = it.message // or any error message as shown above
}
}
formState.valueStream.collectLatest { result ->
btnLogin.enabled = (result is FormResult.Success)
}
btnLogin.setOnClickListener {
viewModel.login(formState.value)
}
}
Validation​
Available Validation Annotations
// String
@EmailAddress
@Optional
@MaxLength(value)
@MinLength(value)
@WebUrl(httpRequired)
// Number
@FloatRange(min, max)
@IntegerRange(min, max)
// Boolean
@IsChecked
// More validations in development
The great thing about form-conductor
is it's very flexible. Each Validation annotation is decoupled from Validation rules.
If you don't like to use annotations, you can use from a list of built-in ValidationRule
instead
// Each rule is associated to respective annotations
EmailAddressRule.validate(value)
FloatRangeRule.validate(value, FloatRange(min,max))
WebUrlRule.validate(value, WebUrl(httpRequired = true))
Custom Validations​
Feeling adventurous or feel like built-in validation rules aren't enough for you?
You can create your own validations rules and annotations to work with form-conductor
instead. You can take advantage of FieldValidation
annotation class and creat your custom annotations and validations.
Please check Custom Validation Guide for full comprehensive guide on custom validations.
// Custom Annotation
@Target(AnnotationTarget.PROPERTY)
@Retention(AnnotationRetention.RUNTIME)
@FieldValidation(
fieldType = LocalDate::class,
validator = FutureDateRule::class
)
annotation class FutureDate
// Custom validation rule
object FutureDateRule : StatelessValdiationRule<LocalDate, FutureDate> {
override fun validate(value: LocalDate, options: FutureDate): FieldResult {
// Your custom validation logic here
}
}
// Usage
// This will automatically work with form-conductor
data class FormData(
@FutureDate
val date: LocalDate
)