Przejdź do treści

Jak zrobić formularz kontaktowy na Androida w Kotlinie i Jetpack Compose

Ekran kontaktowy w Jetpack Compose, który wysyła Ci każdą wiadomość e-mailem przez Twoją własną skrzynkę – bez backendu i bez hasła SMTP w APK. Korzysta z Frontmail Kotlin SDK.

Zaktualizowano:

Film jest po angielsku; poniżej znajdziesz pełny samouczek krok po kroku.

Dodasz ekran kontaktowy do aplikacji na Androida napisanej w Kotlinie i Jetpack Compose. Aplikacja przechowuje tylko klucz publiczny – e-mail wysyła Frontmail przez Twoją skrzynkę, więc w APK nie ląduje żadne hasło SMTP, które ktoś mógłby wyciągnąć.

Potrzebujesz: darmowego konta Frontmail, projektu na Androida z Compose i około piętnastu minut.

1. Podłącz skrzynkę i utwórz szablon

  1. Usługi e-mail → Dodaj usługę: podłącz Gmail, Outlook, SMTP albo dostawcę e-maili transakcyjnych, zaznacz Ustaw jako domyślną, Zweryfikuj i zapisz.
  2. Szablony → Z galerii → Formularz kontaktowy → Użyj szablonu. Parametry wymagane: name, email, message; opcjonalne: phone, topic.
  3. Skopiuj ID szablonu (tpl_…) z zakładki Kod.

2. Zezwól na aplikacje mobilne

  • Bezpieczeństwo → Aplikacje mobilne → Zezwól na aplikacje mobilne → Zapisz. Aplikacje nie wysyłają nagłówka Origin, więc dopóki tego nie zrobisz, API odrzuca je z kodem origin_not_allowed.
  • Szablon → Ustawienia → Ochrona → Wymagaj CAPTCHA (Turnstile). Aplikacje korzystają ze wspólnego widżetu Turnstile Frontmail – klucze Cloudflare nie są potrzebne.

3. Dodaj SDK

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

Dodaj <uses-permission android:name="android.permission.INTERNET" /> do AndroidManifest.xml. Potem skopiuj do projektu funkcje pomocnicze WebView.showTurnstile(…) i WebView.resetTurnstile() z sekcji o Turnstile w dokumentacji Android SDK – to jeden plik, który jest Twój.

4. Zbuduj ekran

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

Błędy sieci, przekroczenia czasu i odpowiedzi 5xx są automatycznie ponawiane z tym samym kluczem idempotencji, więc słaby zasięg nigdy nie sprawi, że dostaniesz tę samą wiadomość dwa razy.

5. Przetestuj

Uruchom aplikację na urządzeniu lub w emulatorze, wypełnij formularz i stuknij Send. E-mail dotrze do Twojej skrzynki i pojawi się w panelu w sekcji Historia.

  • origin_not_allowed – opcja Zezwól na aplikacje mobilne jest wyłączona albo nie została zapisana.
  • captcha_required – nie ma jeszcze tokenu Turnstile; przycisk jest nieaktywny, dopóki weryfikacja się nie zakończy.
  • invalid_template_params – wymagane pole jest puste albo e-mail jest nieprawidłowy.

Skończyły się kredyty? Wiadomość zostanie przyjęta ze statusem HELD i wysłana automatycznie po doładowaniu – sprawdź result.status, jeśli chcesz o tym poinformować użytkownika.

Co dalej

  • Dodaj z galerii szablon Automatyczna odpowiedź: dziękujemy za wiadomość, żeby potwierdzać użytkownikowi otrzymanie wiadomości.
  • Przekaż SendOptions(locale = Locale.getDefault().toLanguageTag()), żeby użyć wersji językowej szablonu w języku telefonu.
Wszystkie samouczki