arrow-exact icon indicating copy to clipboard operation
arrow-exact copied to clipboard

Compose interop

Open CLOVIS-AI opened this issue 1 year ago • 0 comments

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:.

CLOVIS-AI avatar May 20 '23 22:05 CLOVIS-AI