A lightweight Kotlin library for a form state management and field validation.

Overview

Chassis

A lightweight Kotlin library for a form state management and field validation.

Github Actions

Setup

Library and it's snapshots are available on Maven Central repository.

" }">
  // module-level build.gradle
  dependencies {
    implementation "io.github.boguszpawlowski.chassis:chassis:
   
    "
   
  }

The problem

Dealing with complex form logic is a standard task for almost any modern application. When dealing with multiple inputs and outputs it is hard to keep the code clean, concise and consistent across the project. Chassis is a library that tries to solve this problems by offering a lightweight dsl for building your form data, managing it state and propagating the changes to the UI, as well as enabling you to compose your own validation logic.

Concept

The Chassis is a very simple concept. At its core its just a StateFlow of the class representing your form data, with an additional functions for updating the fields and peeking the value.

Advantages

  • Dealing with nullability - your form model can accept null values as initial data, but the library will make sure that after validation, you can access non-nullable values, thus saving you the cumbersome process of null checking.
  • Unopinionated - the library is pure Kotlin (currently only targeting JVM), thus you can use any view system you need.
  • Lightweight - the only dependency is the core coroutine library.
  • Conciseness - traditionally, when dealing with forms, you code can be quickly polluted by updateLogin(), updateEmail() etc. functions. The library has only one function for updating the state, leveraging property reference feature of Kotlin.
  • Support for async validation
  • Build with declarative paradigm in mind.

Usage Guide

Basic example

We are going to build basic login form with client-site validation. For the view system we are going to use Jetpack Compose, but you can use any system you need. If you want to go straight to the code, you can check out the Basic Sample file.

1. Model your form data.

The recommended way is to use Kotlin's data class. In case you don't use it, make sure you have implemented equals function. Every field of the form should be represented by a Field interface where T is the type of the model and V - the type of the data in the field (e.g. String for the email, Boolean for some kind of consent).

  data class LoginForm(
    val login: Field<LoginForm, String>,
    val password: Field<LoginForm, String>,
  )

Since Chassis uses a StateFlow underneath it is critical to implement a valid equals for your function, due to equality-based conflation.

2. Use DSL to make a chassis instance.

The DSL consists of 4 components:

Chassis

The Chassis interface is a sort of manager for your form model. It consists of:

  • state property - a StateFlow with current value of your form model.
  • update - a function for updating any field of the form, by passing an property reference (e.g. LoginForm::login) and the new value.
  • invoke - a function for returning a current value of the form model - it can be used for getting the data after form submit, or just too peek the value (it's just a syntactic sugar for Chassis.state.value )
  • forceValidation - a function for forcing the validation result of a field Async validation.
  • reset - a function for resetting all fields to the initial values.

To create an instance of the Chassis, use the chassis builder function. It accepts value of the type you have provided as a representation of the form data.

  val loginForm = chassis<LoginForm> {
    LoginForm(
      login = TODO(),
      password = TODO(),
    )
  }

Field

The Field interface is the core component of the library. It consists of:

  • value property - current value of the field.
  • isValid - whenever the field is valid or not.
  • isInvalid - whenever the field is invalid or not.
  • invalidReasons - list of all failed validation results.
  • invoke function - it returns a current value of the field - it can be used for getting the data after form submit.

'invoke()' function calls a null-assertion (or cast for nullable fields), so you can only call it once you know the data is valid.

To create an instance of the Field use the field builder function. It accepts a lambda with the FieldBuilderScope receivers, which provides a validators and reduce functions.

  ...
  login = field { TODO() }
  ...

Validators

The field builder function provides a validators() function, which accepts any number of Validator instances. Validator is a functional type, that accepts a value of type T and returns an instance of ValidationResult interface. You can create your own implementation of it, or use some of the validators provided by library. For more information check out the Validation section.

  ...
  login = field { 
    validators(notEmpty(), longerThan(6))
    TODO() 
  }
  ...

Reducer

The field builder function requires from you to return an instance of Reducer interface, which will describe how to modify the model after an field update. The easiest way to provide it is to use reduce higher-order function, which passes current model as a receiver

  typealias Reducer<T, V> = T.(Field<T, V>) -> T

The suggested way to reduce the model is to leverage data class copy function:

  ...
  login = field {
    validators(notEmpty(), longerThan(6))
    reduce { copy(login = it) }
  }
  ...

3. Use invoke() functions to access the data after submit.

  with(loginForm()) { // using invoke function to access current state
    val loginValue: String = login()
    val emailValue: String = email()
  }

Full example

  class MainViewModel(private val register: RegisterUseCase) {
    val loginForm = chassis<LoginForm> {
      LoginForm(
        login = field {
          validators(notEmpty(), longerThan(6))
          reduce { copy(login = it) }
        },
        password = field {
          validators(notEmpty(), longerThan(8))
          reduce { copy(password = it) }
        },
      )
    }
  
    fun onSubmit() {
      with(loginForm()) {
        register(login(), password())
      }
    }
  }

  @Composable
  fun MainScreen(mainViewModel: MainViewModel = MainViewModel()) {
    val form = mainViewModel.loginForm.state.collectAsState()
    Column {
      TextField(
        value = form.login.value.orEmpty(),
        isError = form.login.isInvalid,
        onValueChange = { viewModel.chassis.update(LoginForm::login, it) },
      )
      TextField(
        value = form.password.value.orEmpty(),
        isError = form.password.isInvalid,
        onValueChange = { viewModel.chassis.update(LoginForm::password, it) },
      )
      Button(
        enabled = { form.login.isValid && form.password.isValid },
        onClick = { viewModel.onSubmit() },
      ) {
        Text(text = "Submit")
      }
    }
  }

Validation

You can see that every Field has 2 properties that are determining validation state: isValid and isInvalid. When the user haven't provided any input, the required fields won't be valid nor invalid - they will be in so called Unspecified state. This mechanism is there to prevent your UI from showing error before any user input. For showing a specific error message yous should use invalidReasons property, which consists all failed validation results of your field.

Configuring your validation rules

The basic element of validation configuration is an Validator interface. It's basically a function, that accepts current value and returns an instance of ValidationResult sealed interface. The validation result can either be Valid - concrete object representing correct value, Unspecified - concrete object being a default result for a required field, before any user input or Invalid - an interface representing failed validation. Due to Invalid being an interface, you can return your own implementation of it, which you will be able to map to the specific message in the view layer.

  // your own invalid reason
  data class TooLongInput(val maxLength: Int): Invalid

  // your validator
  fun hasMaxLength(maxLength: Int) = Validator<String?> { value ->
    if (value.orEmpty().size >= maxLength) {
      TooLongInput(maxLength)
    } else {
      Valid
    }
  }

  // pass your validators into the field declaration
  ...
  login = field {
    validators(hasMaxLength(9))
    ...
  }
  ...

Validators composition

If you pass multiple validators into the field declaration, failing only one of them will make whole field invalid. If you want to have field valid when it passes any of the declared validators, you should use or function, which creates a validator, which will be valid even if only one of the combined validators passes.

Advanced Features

Optional Fields

By default all the declared fields will be required - if there is no input they will be invalid. If you want to declare a field that can be optional, you should pass a nullable type in a field declaration.

  data class LoginForm(
    val login: Field<LoginForm, String>, // required
    val password: Field<LoginForm, String>, // required
    val phoneNumber: Field<LoginForm, String?> // optional
  )

Such a field will be valid on no input or nullable input.

When working with text fields you have to remember to not allow for a empty string input for a nullable field. If you don't do that, after user inputs a text and then removes it, the field will always be invalid. E.g. in Jetpack Compose:

  TextField(
    value = form.phoneNumber.value.orEmpty(),
    isError = form.phoneNumber.isInvalid,
    label = { Text(text = "Phone Number (optional)") },
    onValueChange = {
      viewModel.chassis.update(
        field = LoginForm::phoneNumber,
        newValue = it.takeUnless { it.isEmpty() },
      )
    },
  )

Async Validation

The common scenario when dealing with forms is server side validation. For this purpose the concept of Forced Validation is introduced. By using this mechanism you can add validation result completely independently of validators in field declaration.

  val loginForm = chassis<LoginForm> { ... }

  suspend fun validateEmail(email: String) : ValidationResult = api.validateEmail(email)

  fun onSubmitClicked() {
    val emailValidationResult = validateEmail(loginForm().email())
    
    loginForm.forceValidation(LoginForm::email, emailValidationResult)
  }

Such result will be appended to the list of the invalid reasons (if it's invalid) and it will be only present until the next update to the field's value.

License

Copyright 2022 Bogusz Pawłowski

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
You might also like...
A form validation library for android jetpack compose

A form validation library for android jetpack compose

A lightweight state management library for Compose Multiplatform.

Staccato A lightweight state management library for Compose Multiplatform. The term staccato (pronounced "stuh-caw-toe") means detached, or separated,

An Android library to build form and form validations easily.
An Android library to build form and form validations easily.

FormBuilder An Android library to build form and form validations easily. Example COMING SOON Requirements Android 4.3+ Installation Add edit your bui

Android form edit text is an extension of EditText that brings data validation facilities to the edittext.
Android form edit text is an extension of EditText that brings data validation facilities to the edittext.

Android Form EditText Android form edit text is an extension of EditText that brings data validation facilities to the edittext. Example App I built a

Android Material Json Form Wizard is a library for creating beautiful form based wizards within your app just by defining json in a particular format.
Android Material Json Form Wizard is a library for creating beautiful form based wizards within your app just by defining json in a particular format.

Android Json Wizard Android Json Wizard is a library for creating beautiful form based wizards within your app just by defining json in a particular f

Simple-Claim-Form - Android App for creating a simple dynamic form with MVVM architecture

Simple-Claim-Form Android App for creating a simple dynamic form with MVVM archi

[ACTIVE] Simple Stack, a backstack library / navigation framework for simpler navigation and state management (for fragments, views, or whatevers).
[ACTIVE] Simple Stack, a backstack library / navigation framework for simpler navigation and state management (for fragments, views, or whatevers).

Simple Stack Why do I want this? To make navigation to another screen as simple as backstack.goTo(SomeScreen()), and going back as simple as backstack

Android-kotlin-lifecycles-management - LifeCycles Management Techniques
Android-kotlin-lifecycles-management - LifeCycles Management Techniques

Activity Lifecycle - DessertPusher This is the toy app for lesson 4 of the Andro

Sample application to show state management & unidirectional data flow using Jetpack compose.
Sample application to show state management & unidirectional data flow using Jetpack compose.

Jetpack Compose State Management A sample project to demonstrate State Management in Jetpack compose by following This CodeLab. Through the project yo

“Budgeter” is an optimized application for personal account management. Household account management is complicated.
“Budgeter” is an optimized application for personal account management. Household account management is complicated.

Budgeter Links 🔗 Open-Source Library MVVM-Architecture Viewmodel-LiveData Room Database Dependency Injection-Hilt Coroutines Things we used while mak

An app that is a one-stop destination for all the CS enthusiasts, providing resources like Information scrapping techniques, best YT channels, courses available free-of-cost, etc.  & knowledge about every domain and field that exists on the Internet related to Computer Science along with News, Jobs, and Internships opportunities in these domains along with valuable tips and hacks from mentors for a particular domain.
An app that is a one-stop destination for all the CS enthusiasts, providing resources like Information scrapping techniques, best YT channels, courses available free-of-cost, etc. & knowledge about every domain and field that exists on the Internet related to Computer Science along with News, Jobs, and Internships opportunities in these domains along with valuable tips and hacks from mentors for a particular domain.

An app that is a one-stop destination for all the CS enthusiasts, providing resources like Information scrapping techniques, best YT channels, courses available free-of-cost, etc. & knowledge about every domain and field that exists on the Internet related to Computer Science along with News, Jobs, and Internships opportunities in these domains along with valuable tips and hacks from mentors for a particular domain.

Material Design text field that comes in a box, based on (OLD) Google Material Design guidelines.
Material Design text field that comes in a box, based on (OLD) Google Material Design guidelines.

TextFieldBoxes A new Material Design text field that comes in a box, based on Google Material Design guidelines. 🇨🇳 中文看这里 UPDATE NOTICE 1.4.5 Releas

An address-autocompleting text field for Android
An address-autocompleting text field for Android

android-PlacesAutocompleteTextView An AutocompleteTextView that interacts with the Google Maps Places API to provide location results and caches selec

A module designed to encapsulate the use of an Android EditText field for gathering currency information from a user. Supports all ISO-3166 compliant locales/currencies.
A module designed to encapsulate the use of an Android EditText field for gathering currency information from a user. Supports all ISO-3166 compliant locales/currencies.

CurrencyEditText CurrencyEditText is an extension of Android's EditText view object. It is a module designed to provide ease-of-use when using an Edit

Automatically filled the declared non-null field is missing or null with default value.

[TOC] Moshi-kotlin-nullsafe 中文版 1. moshi-kotlin moshi-kotlin support kotlin type safe check. When parsing json, fields declared as non-null types may

Flavor - A lightweight Kotlin dependency injection & service management framework.

flavor A light-weight kotlin dependency injection & service management framework. Flavor is based around guice's design. Usage: Creating a Flavor inst

Android validation library which helps developer boil down the tedious work to three easy steps.
Android validation library which helps developer boil down the tedious work to three easy steps.

AwesomeValidation Introduction Implement validation for Android within only 3 steps. Developers should focus on their awesome code, and let the librar

Compose easy forms validation library

Compose EasyForms Focus on building your form UI while the library do the heavy work for you. Features Built in support for most of the Form widgets i

Kotlin validation with a focus on readability
Kotlin validation with a focus on readability

kommodus Kotlin validation with a focus on readability import com.github.kommodus.constraints.* import com.github.kommodus.Validation data class Test

Releases(v0.1.0)
Owner
Bogusz Pawłowski
Bogusz Pawłowski
Android Library to make it easy to create CodeEditor or IDE that support any languages and themes

CodeView Android Library to make it easy to create your CodeEditor or IDE for any programming language even for your programming language, just config

Amr Hesham 294 Jan 6, 2023
A periodic text updating library

RotatingText Rotating text is an Android library that can be used to make text switching painless and beautiful, with the use of interpolators, typefa

Mobile Development Group 1.6k Dec 30, 2022
Include MatchTextView and MatchButton..Come..you will like it

Android MatchView This project is learned from (https://github.com/liaohuqiu/android-Ultra-Pull-To-Refresh) . Thanks for liaohuqiu.. I like the animat

Roger 858 Dec 7, 2022
Field state manager and basic set of validation, fields

Compose Forms Field state manager and basic set of validation, fields

Vitaliy Zarubin 9 Jan 4, 2022
A simple, lightweight and powerful field validation library for Android.

Convalida Convalida - (Italian for "validation") Convalida is a simple, lightweight and powerful field validation library for Android. Documentation G

Wellington Costa 203 Nov 18, 2022
A lightweight, simplified form validation library for Android

A lightweight, simplified form validation library for Android

İbrahim Süren 91 Nov 9, 2022
Date text field with on the fly validation built with Jetpack Compose.

Date text field with on the fly validation built with Jetpack Compose.

null 15 Nov 16, 2022
Form validation and feedback library for Android. Provides .setText for more than just TextView and EditText widgets. Provides easy means to validate with dependencies.

android-formidable-validation Form validation and feedback library for Android. Provides .setText for more than just TextView and EditText widgets. Pr

Linden 147 Nov 20, 2022
UI form validation library for Android

Android Saripaar v2 சரிபார் - sari-paar (Tamil for "to check", "verify" or "validate") Android Saripaar is a simple, feature-rich and powerful rule-ba

Ragunath Jawahar 3.2k Dec 29, 2022