One Off the Slack: Where Should Validation Go?
Colton Idle asked:
My design system is calling for 3 types of input fields.
- Plain text. You can enter anything
- Email field. The input field should do basic validation to make sure it’s an email (include @ sign, etc)
- Phone number field. The input field should “validate” that it’s a phone number (requires numbers, etc)
I don’t want to get into the topic of what is/isn’t a valid phone/email. The topic to discuss is whether or not to bake these validation INTO the composable themselves.
So we have something like this.
Column{
BasicInput()
EmailInput()
PhoneInput()
}
A few team members have argued that the validation shouldn’t be baked into a composable, so we originally were doing all checks in a ViewModel and not in the composable, but the issue is, someone builds a new screen, the add EmailInput() but forgets to add the validation in the VM for the new screen and now we’re back at square 1.
What would you do?
There are two obvious answers, and one less-obvious one.
One obvious answer is to put the validation logic in the composable, so EmailInput()
knows what the validation rules are.
Another obvious answer, and the one that Colton’s team members were arguing for, is that the validation goes in a viewmodel or some similar construct.
Tash suggested a third answer: the validation rules go in a custom state object:
If each Composable is defined by a set of requirements that call out its existence in the first place, then it should come with its own corresponding State class that implements all the smarts.
So for example, the very reason one might want an EmailInput() is because they want “email specific text” and other related behaviors to be packaged along with the UI element in some way. So, for an EmailInput() Composable, you could create an EmailInputState
interface EmailInputState { /** handles validations, internal text highlighting, etc **/ }
and then your component could do something like this:
@Composable fun EmailInput(state: EmailInputState, ....)
add default param value of
state = remember { EmailInputState() }
if the component defines some default policies. this way you’d be hoisting the validation info as well.
Tash linked to documentation on hoisted states, though the subsequent section on policies in state objects may be even more on point.
In the end, as any senior developer will tell you, “it depends”.
For intrinsic validation, Tash’s approach seems to be the most flexible while still being easily reusable. By “intrinsic”, I mean that the validation is wholly determined by the composable and its state for the composable. However, for extrinsic validation – where something outside the composable and its state is the only thing that knows about whether the input is valid – a viewmodel (or the equivalent) is inevitable.
For example:
-
Highlighting a syntax error in an email address, where the text entered cannot be a valid email address (e.g., no
@
), is intrinsic validation -
Highlighting that the supplied email address is not a recognized account in your Web service is extrinsic validation
IOW, a composable or its state object should not be trying to do disk or network I/O as part of validation. A state object might be told of an extrinsic validation failure, and the composable can render that failure as part of rendering the state. However, the actual validation rule needs to be somewhere more clearly separated from the act of rendering that UI.
Read the original thread in the kotlinlang Slack workspace. Not a member? Join that Slack workspace here!