Come creare un modulo di contatto su Android con Kotlin e Jetpack Compose
Una schermata di contatto in Jetpack Compose che ti invia ogni messaggio tramite la tua casella di posta: niente backend, nessuna password SMTP nell’APK. Usa l’SDK Kotlin di Frontmail.
Aggiornato:
Aggiungerai una schermata di contatto a un’app Android scritta in Kotlin e Jetpack Compose. L’app conserva solo una chiave pubblica: l’email la invia Frontmail tramite la tua casella, quindi nell’APK non finisce nessuna password SMTP che chiunque potrebbe estrarre.
Ti serve: un account Frontmail gratuito, un progetto Android con Compose e circa un quarto d’ora.
1. Collega la tua casella e crea il modello
- Servizi email → Aggiungi servizio: collega Gmail, Outlook, SMTP o un provider transazionale, seleziona Imposta come predefinito, poi Verifica e salva.
- Modelli → Dalla galleria → Modulo di contatto → Usa modello. Parametri
obbligatori:
name,email,message; facoltativi:phone,topic. - Copia l’ID del modello (
tpl_…) dalla scheda Snippet.
2. Consenti le app mobile
- Sicurezza → App mobile → Consenti app mobile → Salva. Le app non inviano l’header
Origin, quindi finché non lo fai l’API le rifiuta conorigin_not_allowed. - Modello → Impostazioni → Protezione → Richiedi CAPTCHA (Turnstile). Le app usano il widget Turnstile condiviso di Frontmail: non servono chiavi Cloudflare.
3. Aggiungi l’SDK
// app/build.gradle.kts
dependencies {
implementation("dev.frontmail:frontmail:0.1.0")
}
Aggiungi <uses-permission android:name="android.permission.INTERNET" /> ad AndroidManifest.xml.
Poi copia nel tuo progetto gli helper WebView.showTurnstile(…) e WebView.resetTurnstile() dalla
sezione Turnstile della documentazione dell’SDK Android: è un solo file, ed è tuo.
4. Crea la schermata
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
val frontmail = Frontmail("pk_…") // create once, reuse (thread-safe)
@Composable
fun ContactScreen() {
var name by remember { mutableStateOf("") }
var email by remember { mutableStateOf("") }
var message 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())
OutlinedTextField(message, { message = it }, label = { Text("Message") }, 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,
onClick = {
scope.launch {
try {
frontmail.send(
templateId = "tpl_…",
params = mapOf("name" to name, "email" to email, "message" to message),
options = SendOptions(turnstileToken = token, turnstileKey = widget?.turnstileKey),
)
info = "Thank you! We’ll get back to you soon."
} catch (e: FrontmailException) {
info = e.message ?: "Sending failed"
} finally {
token = null // a token works only once
webView?.resetTurnstile()
}
}
},
) { Text("Send") }
Text(info)
}
}
Gli errori di rete, i timeout e le risposte 5xx vengono ritentati automaticamente con la stessa
chiave di idempotenza, quindi un segnale debole non ti manda mai due volte lo stesso messaggio.
5. Provalo
Avvia l’app su un dispositivo o sull’emulatore, compila il modulo e tocca Send. L’email arriva nella tua casella e compare in Cronologia nella dashboard.
origin_not_allowed– Consenti app mobile è disattivato o non è stato salvato.captcha_required– non c’è ancora un token Turnstile; il pulsante resta disattivato finché la verifica non finisce.invalid_template_params– un campo obbligatorio è vuoto oppure l’email non è valida.
Crediti esauriti? Il messaggio viene accettato con lo stato
HELDe inviato automaticamente appena ricarichi: controllaresult.statusse vuoi avvisare l’utente.
Prossimi passi
- Aggiungi dalla galleria il modello Risposta automatica: grazie per il messaggio per confermare il messaggio all’utente.
- Passa
SendOptions(locale = Locale.getDefault().toLanguageTag())per usare la variante linguistica del modello nella lingua del telefono.


