Skip to content

Compose interop #18

@CLOVIS-AI

Description

@CLOVIS-AI

Here's a pattern I have seen multiple times in Compose application (and personally use 😄).

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 Passworddata 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 😇.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions