arrow-exact
arrow-exact copied to clipboard
Compose interop
Here's a pattern I have seen multiple times in Compose application (and personally use :smile:).
Current situation
We have a form with two values, a username and a password.
We have immutable domain objects created using arrow-exact:
@JvmInline value class Username …
@JvmInline value class Password …
data class LogInForm(
val username: Username,
val password: Password,
)
However, UI forms have to be mutable. So we need another object to store the values before they are validated:
class MutableLogInForm(
username: String = "",
password: String = "",
) {
constructor(previous: LogIn) : this(previous.username.text, previous.password.text)
// Understanding what mutableStateOf does is not really important here,
// except that it's important for Compose to encapsulate and control mutability
var username by mutableStateOf(username)
var password by mutableStateOf(password)
fun toImmutable() = either {
LogInForm(
Username.from(username).bind(),
Password.from(password).bind(),
)
}
}
This is not too bad, but there are a few downsides:
- Fields are validated in order (we don't use
EitherNel
) - If validation fails, we don't know which field is the source of the failure
- If we want to use
EitherNel
, we have to split the validation in multiple sub-functions to identify which field is invalid
Proposal
Ideally, I'm imagining some utility class like this:
abstract class ExactForm {
private val fields = ArrayList<MutableExactState<*…>>()
protected fun <T, R…> validate(initial: T, construct: ExactScope<…>.() -> R): MutableExactState<T, …> = …
val allErrors get() = fields.mapOrAccumulate { … }
val valid get() = allErrors.all { it.valid }
}
Which allows to declare forms like this:
class MutableLogInForm(
username: String = "",
password: String = "",
) {
constructor(previous: LogIn) : this(previous.username.text, previous.password.text)
val username = validate(username) {
// Full power of the Exact DSL
ensure(Username)
}
val password = validate(password) {
ensure(Password)
}
fun toImmutable() = either {
LogInForm(
username.validate().bind(),
password.validate().bind(),
)
}
}
Usage:
@Composable
fun LogInForm() {
val form = remember { MutableLogInForm() }
// 'raw' usage
TextField(
label = "Username",
value = form.username.value,
onChange = { form.username.value = it },
failureText = form.username.failure?.toString(),
)
// assuming an appropriate overload which binds everything
PasswordField(
label = "Password",
exactState = form.password,
)
SubmitButton(
onClick = {
val login = form.toImmutable()
logInWith(login)
}
enabled = form.valid,
)
}
If we decide this is out of scope for this project, I'll probably end up implementing it as an optional extra module of Decouple, so I'm interested in your opinions anyway :innocent:.