Exemplos de Código: Capítulo 08 — Formulários e Validação no Compose

Especialização em Mobile Nativo com Android • Demonstrações Práticas Compiláveis


💻 Código de Demonstração: Formulário de Autenticação com Validação Reativa e FocusManager

O código abaixo demonstra o desenvolvimento de formulários profissionais no Jetpack Compose:

  1. Validação reativa de campos de texto (E-mail corporativo e Senha com entropia mínima).
  2. Alternância de visibilidade de senha com PasswordVisualTransformation.
  3. Navegação fluida de foco entre campos através de FocusRequester e ImeAction.Next / ImeAction.Done.
  4. Exibição de mensagens de erro acessíveis via supportingText do Material 3.
package br.com.portal.mobile.android.cap08

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Email
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp

// =====================================================================
// 1. Regras Puras de Validação de Domínio
// =====================================================================

object ValidadorFormulario {
    fun validarEmail(email: String): String? {
        if (email.isBlank()) return "E-mail não pode ser vazio."
        val padrao = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[a-z]{2,}$".toRegex()
        return if (padrao.matches(email)) null else "Formato de e-mail inválido."
    }

    fun validarSenha(senha: String): String? {
        if (senha.length < 8) return "Senha deve ter no mínimo 8 caracteres."
        if (!senha.any { it.isDigit() }) return "Senha deve conter pelo menos um dígito."
        if (!senha.any { it.isUpperCase() }) return "Senha deve conter pelo menos uma letra maiúscula."
        return null
    }
}

// =====================================================================
// 2. Formulário Interativo
// =====================================================================

@Composable
fun FormularioLoginScreen(
    onLoginSucesso: (String, String) -> Unit,
    modifier: Modifier = Modifier
) {
    var email by rememberSaveable { mutableStateOf("") }
    var senha by rememberSaveable { mutableStateOf("") }
    var senhaVisivel by rememberSaveable { mutableStateOf(false) }

    // Estados de Erro
    var erroEmail by remember { mutableStateOf<String?>(null) }
    var erroSenha by remember { mutableStateOf<String?>(null) }

    // Gestão de Foco
    val focusManager = LocalFocusManager.current
    val senhaFocusRequester = remember { FocusRequester() }

    Surface(
        modifier = modifier.fillMaxSize(),
        color = MaterialTheme.colorScheme.background
    ) {
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(24.dp)
        ) {
            Text(
                text = "Acesso Corporativo",
                style = MaterialTheme.typography.headlineLarge,
                color = MaterialTheme.colorScheme.primary
            )
            Text(
                text = "Insira suas credenciais seguras para autenticar.",
                style = MaterialTheme.typography.bodyMedium,
                color = MaterialTheme.colorScheme.onSurfaceVariant
            )

            Spacer(modifier = Modifier.height(32.dp))

            // Campo de E-mail
            OutlinedTextField(
                value = email,
                onValueChange = {
                    email = it
                    erroEmail = ValidadorFormulario.validarEmail(it)
                },
                label = { Text("E-mail Profissional") },
                placeholder = { Text("usuario@portal.com") },
                leadingIcon = { Icon(Icons.Default.Email, contentDescription = null) },
                isError = erroEmail != null,
                supportingText = { erroEmail?.let { Text(it) } },
                singleLine = true,
                keyboardOptions = KeyboardOptions(
                    keyboardType = KeyboardType.Email,
                    imeAction = ImeAction.Next
                ),
                keyboardActions = KeyboardActions(
                    onNext = { senhaFocusRequester.requestFocus() }
                ),
                modifier = Modifier.fillMaxWidth()
            )

            Spacer(modifier = Modifier.height(16.dp))

            // Campo de Senha com Visibilidade Alternável
            OutlinedTextField(
                value = senha,
                onValueChange = {
                    senha = it
                    erroSenha = ValidadorFormulario.validarSenha(it)
                },
                label = { Text("Senha") },
                leadingIcon = { Icon(Icons.Default.Lock, contentDescription = null) },
                trailingIcon = {
                    IconButton(onClick = { senhaVisivel = !senhaVisivel }) {
                        Icon(
                            imageVector = if (senhaVisivel) Icons.Default.VisibilityOff else Icons.Default.Visibility,
                            contentDescription = if (senhaVisivel) "Ocultar senha" else "Mostrar senha"
                        )
                    }
                },
                visualTransformation = if (senhaVisivel) VisualTransformation.None else PasswordVisualTransformation(),
                isError = erroSenha != null,
                supportingText = { erroSenha?.let { Text(it) } },
                singleLine = true,
                keyboardOptions = KeyboardOptions(
                    keyboardType = KeyboardType.Password,
                    imeAction = ImeAction.Done
                ),
                keyboardActions = KeyboardActions(
                    onDone = { focusManager.clearFocus() }
                ),
                modifier = Modifier
                    .fillMaxWidth()
                    .focusRequester(senhaFocusRequester)
            )

            Spacer(modifier = Modifier.height(32.dp))

            // Botão de Submissão Condicional
            val formularioValido = email.isNotBlank() && senha.isNotBlank() && erroEmail == null && erroSenha == null
            Button(
                onClick = {
                    focusManager.clearFocus()
                    onLoginSucesso(email, senha)
                },
                enabled = formularioValido,
                shape = RoundedCornerShape(12.dp),
                modifier = Modifier
                    .fillMaxWidth()
                    .height(50.dp)
            ) {
                Text(text = "Entrar no Sistema")
            }
        }
    }
}

@Preview(showBackground = true)
@Composable
fun FormularioPreview() {
    MaterialTheme {
        FormularioLoginScreen(onLoginSucesso = { _, _ -> })
    }
}