Comment créer un formulaire de demande de devis Android avec Kotlin
Un formulaire de demande de devis Jetpack Compose avec choix de la prestation et du budget et une échéance, qui vous envoie chaque demande par e-mail et envoie au client une confirmation automatique.
Mis à jour:
Ce tutoriel crée un écran de demande de devis avec Jetpack Compose. Le client choisit une prestation et un budget, saisit une échéance et une description ; vous recevez un e-mail structuré et le client une confirmation automatique.
Il vous faut : un compte Frontmail gratuit, un projet Android avec Compose et une vingtaine de minutes.
1. Connectez votre boîte mail et créez les modèles
- Services d’envoi → Ajouter un service : connectez votre boîte mail ou votre fournisseur, Définir par défaut, puis Vérifier et enregistrer.
- Modèles → Depuis la galerie → Demande de devis (catégorie Entreprise) → Utiliser ce modèle.
- Depuis la galerie → Réponse automatique : merci pour votre message → Utiliser ce
modèle ; définissez les valeurs par défaut de
company_nameetresponse_time, puis sélectionnez-le dans le modèle de devis sous Paramètres → Envoi → Modèle de réponse automatique.
Le modèle de devis requiert name, email, service et details ; phone, company, budget
et deadline (YYYY-MM-DD) sont facultatifs. service et budget n’acceptent que les valeurs
listées dans Paramètres. Le code ci-dessous utilise les valeurs du modèle anglais ; si vous
créez le modèle en français, ce sont Site web, Boutique en ligne, Application mobile, Conseil,
Autre et Moins de 5 000 €, 5 000 – 20 000 €, 20 000 – 50 000 €, Plus de 50 000 €.
L’application doit envoyer exactement les chaînes qui y figurent.
2. Autorisez les applications mobiles et activez le CAPTCHA
- Sécurité → Applications mobiles → Autoriser les applications mobiles → Enregistrer.
- Modèle de devis → Paramètres → Protection → Exiger un CAPTCHA (Turnstile) (widget mobile partagé de Frontmail, aucune clé nécessaire).
3. Ajoutez le SDK
// app/build.gradle.kts
dependencies {
implementation("dev.frontmail:frontmail:0.1.0")
}
Ajoutez la permission INTERNET et copiez les helpers showTurnstile / resetTurnstile depuis la
section Turnstile de la documentation du SDK Android.
4. Créez l’écran
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)
}
}
Les valeurs null (pas de budget, pas d’échéance valide) sont omises de l’e-mail. Remplacez le
champ texte par un DatePicker Material 3 si vous préférez – envoyez simplement la date au format
YYYY-MM-DD.
5. Testez
Envoyez une demande avec votre propre adresse e-mail : vous recevez la demande puis, un instant plus tard, la confirmation. Les deux apparaissent dans l’Historique avec leur chronologie d’envoi.
Une demande et sa confirmation consomment deux crédits. Si vos crédits s’épuisent, les demandes sont acceptées avec le statut
HELDet envoyées automatiquement après la recharge.
Étapes suivantes
- Activez la Collecte des contacts dans les paramètres du modèle pour garder la liste de toutes les personnes qui ont demandé un devis.
- Joignez des fichiers (par exemple un cahier des charges en PDF) avec
SendOptions(attachments = …)– voir la documentation du SDK.


