Zum Inhalt springen

Kontaktformular in Android mit Kotlin und Jetpack Compose erstellen

Ein Kontaktbildschirm in Jetpack Compose, der Ihnen jede Nachricht über Ihr eigenes Postfach schickt – ohne Backend und ohne SMTP-Passwort in der APK. Mit dem Frontmail-Kotlin-SDK.

Aktualisiert:

Das Video ist auf Englisch; die Schritte unten sind das vollständige Tutorial.

Sie fügen einer Android-App in Kotlin und Jetpack Compose einen Kontaktbildschirm hinzu. Die App enthält nur einen öffentlichen Schlüssel – die E-Mail versendet Frontmail über Ihr Postfach, sodass kein SMTP-Passwort in der APK landet, wo es jeder auslesen könnte.

Sie brauchen: ein kostenloses Frontmail-Konto, ein Android-Projekt mit Compose und etwa fünfzehn Minuten.

1. Postfach verbinden und Vorlage anlegen

  1. E-Mail-Dienste → Dienst hinzufügen: Verbinden Sie Gmail, Outlook, SMTP oder einen Transaktions-Anbieter, aktivieren Sie Als Standard festlegen, dann Prüfen & speichern.
  2. Vorlagen → Aus der Galerie → Kontaktformular → Vorlage verwenden. Pflichtparameter: name, email, message; optional: phone, topic.
  3. Kopieren Sie die Vorlagen-ID (tpl_…) aus dem Tab Snippets.

2. Mobile Apps erlauben

  • Sicherheit → Mobile Apps → Mobile Apps erlauben → Speichern. Apps senden keinen Origin-Header, deshalb lehnt die API sie bis dahin mit origin_not_allowed ab.
  • Vorlage → Einstellungen → Schutz → CAPTCHA verlangen (Turnstile). Apps nutzen das gemeinsame Turnstile-Widget von Frontmail – keine Cloudflare-Schlüssel nötig.

3. SDK hinzufügen

// app/build.gradle.kts
dependencies {
    implementation("dev.frontmail:frontmail:0.1.0")
}

Ergänzen Sie <uses-permission android:name="android.permission.INTERNET" /> in AndroidManifest.xml. Kopieren Sie dann die Hilfsfunktionen WebView.showTurnstile(…) und WebView.resetTurnstile() aus dem Turnstile-Abschnitt der Android-SDK-Dokumentation in Ihr Projekt – eine einzige Datei, die Ihnen gehört.

4. Bildschirm bauen

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)
    }
}

Netzwerkfehler, Timeouts und 5xx-Antworten werden automatisch mit demselben Idempotenzschlüssel wiederholt – bei schwachem Empfang bekommen Sie dieselbe Nachricht also nie doppelt.

5. Testen

Starten Sie die App auf einem Gerät oder Emulator, füllen Sie das Formular aus und tippen Sie auf Send. Die E-Mail kommt in Ihrem Postfach an und steht im Dashboard unter Verlauf.

  • origin_not_allowed – Mobile Apps erlauben ist ausgeschaltet oder wurde nicht gespeichert.
  • captcha_required – noch kein Turnstile-Token; der Button bleibt deaktiviert, bis die Prüfung abgeschlossen ist.
  • invalid_template_params – ein Pflichtfeld ist leer oder die E-Mail-Adresse ungültig.

Keine Credits mehr? Die Nachricht wird mit dem Status HELD angenommen und nach dem Aufladen automatisch versendet – prüfen Sie result.status, wenn Sie den Nutzer informieren möchten.

Nächste Schritte

  • Bestätigen Sie dem Nutzer die Nachricht mit der Vorlage Automatische Antwort: Danke für Ihre Nachricht aus der Galerie.
  • Übergeben Sie SendOptions(locale = Locale.getDefault().toLanguageTag()), um die Sprachvariante der Vorlage für die Sprache des Telefons zu verwenden.
Alle Tutorials