Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compose interop #18

Open
CLOVIS-AI opened this issue May 20, 2023 · 0 comments
Open

Compose interop #18

CLOVIS-AI opened this issue May 20, 2023 · 0 comments

Comments

@CLOVIS-AI
Copy link

CLOVIS-AI commented May 20, 2023

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant