Cómo crear un formulario de solicitud de presupuesto en Android con Kotlin
Un formulario de presupuesto en Jetpack Compose con opciones de servicio y presupuesto y una fecha límite que te envía cada solicitud por correo y manda al cliente una confirmación automática.
Actualizado:
En este tutorial crearás una pantalla de solicitud de presupuesto en Jetpack Compose. El cliente elige un servicio y un presupuesto, indica una fecha límite y una descripción; tú recibes un correo estructurado y el cliente, una confirmación automática.
Necesitas: una cuenta gratuita de Frontmail, un proyecto Android con Compose y unos veinte minutos.
1. Conecta tu buzón y crea las plantillas
- Servicios de correo → Añadir servicio: conecta tu buzón o proveedor, Marcar como predeterminado, Verificar y guardar.
- Plantillas → Desde la galería → Solicitud de presupuesto (categoría Negocios) → Usar plantilla.
- Desde la galería → Respuesta automática: gracias por tu mensaje → Usar plantilla;
define valores por defecto para
company_nameyresponse_timey selecciónala en la plantilla de presupuesto en Ajustes → Entrega → Plantilla de respuesta automática.
La plantilla de presupuesto necesita name, email, service y details; phone, company,
budget y deadline (YYYY-MM-DD) son opcionales. service y budget solo aceptan los valores
de la pestaña Parámetros. El código de ejemplo usa los de la plantilla en inglés; si creas la
plantilla en español, son Sitio web, Tienda online, App móvil, Consultoría, Otro y Menos de
5000 €, 5000–20 000 €, 20 000–50 000 €, Más de 50 000 €. Usa en la app exactamente los mismos
textos.
2. Permite las apps móviles y activa la CAPTCHA
- Seguridad → Apps móviles → Permitir apps móviles → Guardar.
- Plantilla de presupuesto → Ajustes → Protección → Exigir CAPTCHA (Turnstile) (el widget móvil compartido de Frontmail, sin necesidad de claves).
3. Añade el SDK
// app/build.gradle.kts
dependencies {
implementation("dev.frontmail:frontmail:0.1.0")
}
Añade el permiso INTERNET y copia los helpers showTurnstile / resetTurnstile de la sección
Turnstile de la documentación del SDK de Android.
4. Crea la pantalla
import android.webkit.WebView
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import dev.frontmail.Frontmail
import dev.frontmail.FrontmailException
import dev.frontmail.SendOptions
import dev.frontmail.Turnstile
import dev.frontmail.TurnstileWidget
import kotlinx.coroutines.launch
import java.time.LocalDate
val frontmail = Frontmail("pk_…")
val services = listOf("Website", "E-shop", "Mobile app", "Consulting", "Other")
val budgets = listOf("Under $5k", "$5k–20k", "$20k–50k", "Over $50k")
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun Choices(options: List<String>, selected: String?, onSelect: (String?) -> Unit) {
FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
options.forEach { option ->
FilterChip(
selected = option == selected,
onClick = { onSelect(if (option == selected) null else option) },
label = { Text(option) },
)
}
}
}
@Composable
fun QuoteScreen() {
var name by remember { mutableStateOf("") }
var email by remember { mutableStateOf("") }
var service by remember { mutableStateOf<String?>(services.first()) }
var budget by remember { mutableStateOf<String?>(null) }
var deadline by remember { mutableStateOf("") }
var details by remember { mutableStateOf("") }
var widget by remember { mutableStateOf<TurnstileWidget?>(null) }
var token by remember { mutableStateOf<String?>(null) }
var webView by remember { mutableStateOf<WebView?>(null) }
var info by remember { mutableStateOf("") }
val scope = rememberCoroutineScope()
LaunchedEffect(Unit) { widget = Turnstile.sharedWidget() }
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
OutlinedTextField(name, { name = it }, label = { Text("Name") }, modifier = Modifier.fillMaxWidth())
OutlinedTextField(email, { email = it }, label = { Text("Email") }, modifier = Modifier.fillMaxWidth())
Text("What do you need?")
Choices(services, service) { service = it }
Text("Budget (optional)")
Choices(budgets, budget) { budget = it }
OutlinedTextField(deadline, { deadline = it }, label = { Text("Deadline (YYYY-MM-DD, optional)") }, modifier = Modifier.fillMaxWidth())
OutlinedTextField(details, { details = it }, label = { Text("Project details") }, minLines = 4, modifier = Modifier.fillMaxWidth())
widget?.let { w ->
AndroidView(
factory = { context ->
WebView(context).apply {
showTurnstile(w, onToken = { token = it }, onExpire = { token = null })
webView = this
}
},
modifier = Modifier.fillMaxWidth().height(70.dp),
)
}
Button(
enabled = token != null && service != null,
onClick = {
scope.launch {
try {
frontmail.send(
templateId = "tpl_…",
params = mapOf(
"name" to name,
"email" to email,
"service" to service,
"budget" to budget,
"deadline" to runCatching { LocalDate.parse(deadline).toString() }.getOrNull(),
"details" to details,
),
options = SendOptions(turnstileToken = token, turnstileKey = widget?.turnstileKey),
)
info = "Thanks! We’ve emailed you a confirmation."
} catch (e: FrontmailException) {
info = e.message ?: "Sending failed"
} finally {
token = null // a token works only once
webView?.resetTurnstile()
}
}
},
) { Text("Request a quote") }
Text(info)
}
}
Los valores null (sin presupuesto, sin una fecha límite válida) se omiten en el correo. Si lo
prefieres, cambia el campo de texto por un DatePicker de Material 3; solo tienes que enviar la
fecha como YYYY-MM-DD.
5. Pruébalo
Envía una solicitud con tu propio correo: recibirás la solicitud y, un momento después, la confirmación. Ambas aparecen en Historial con la cronología de su entrega.
Una solicitud con su confirmación consume dos créditos. Si se te acaban los créditos, las solicitudes se aceptan como
HELDy se envían automáticamente cuando recargas.
Próximos pasos
- Activa Recogida de contactos en los ajustes de la plantilla para tener una lista de todas las personas que han pedido presupuesto.
- Adjunta archivos (por ejemplo, un briefing en PDF) con
SendOptions(attachments = …); consulta la documentación del SDK.


