Anfrageformular in Flutter erstellen – Angebotsanfrage per E-Mail
Ein Flutter-Anfrageformular mit Leistungsauswahl, Budget und Termin, das Ihnen jede Anfrage per E-Mail schickt und dem Kunden automatisch eine Bestätigung sendet – ganz ohne Backend.
Aktualisiert:
In diesem Tutorial bauen Sie einen Bildschirm für Angebotsanfragen in Flutter: Der Kunde wählt eine Leistung, ein Budget und einen Termin, beschreibt sein Projekt und tippt auf Send. Sie erhalten eine sauber formatierte E-Mail, der Kunde eine automatische Bestätigung.
Sie brauchen: ein kostenloses Frontmail-Konto, ein Flutter-Projekt ab 3.24 und etwa zwanzig Minuten.
1. Postfach verbinden und Vorlagen anlegen
- E-Mail-Dienste → Dienst hinzufügen: Verbinden Sie Ihr Postfach oder Ihren Anbieter, aktivieren Sie Als Standard festlegen, dann Prüfen & speichern.
- Vorlagen → Aus der Galerie → Angebotsanfrage (Kategorie Geschäftlich) → Vorlage verwenden.
- Bestätigung hinzufügen: Aus der Galerie → Automatische Antwort: Danke für Ihre
Nachricht → Vorlage verwenden, auf ihrem Tab Parameter Standardwerte für
company_nameundresponse_timefestlegen und sie dann in der Anfragevorlage unter Einstellungen → Zustellung → Vorlage für automatische Antwort auswählen.
Die Anfragevorlage erwartet name, email, service und details (Pflicht) sowie phone,
company, budget und deadline (optional). service und budget akzeptieren nur die Werte auf
dem Tab Parameter – passen Sie die Listen an Ihr Angebot an und verwenden Sie in der App
dieselben Zeichenketten. Das Codebeispiel nutzt die Werte der englischen Vorlage; eine auf Deutsch
angelegte Vorlage hat Website, Onlineshop, Mobile App, Beratung, Sonstiges und Unter 5.000 €,
5.000–20.000 €, 20.000–50.000 €, Über 50.000 €. Die App muss genau die Werte vom Tab Parameter
senden.
2. Mobile Apps erlauben und CAPTCHA aktivieren
- Sicherheit → Mobile Apps → Mobile Apps erlauben → Speichern. Ohne diese
Einstellung werden Anfragen ohne Website-
Originmitorigin_not_allowedabgelehnt. - Anfragevorlage → Einstellungen → Schutz → CAPTCHA verlangen (Turnstile). Apps nutzen das gemeinsame Widget von Frontmail – kein Cloudflare-Konto nötig.
3. Installieren
flutter pub add frontmail webview_flutter url_launcher
Kopieren Sie TurnstileView aus dem Turnstile-Abschnitt der Flutter-SDK-Dokumentation nach
lib/turnstile_view.dart.
4. Formular bauen
import 'package:flutter/material.dart';
import 'package:frontmail/frontmail.dart';
import 'turnstile_view.dart';
final frontmail = Frontmail(publicKey: 'pk_…');
const services = ['Website', 'E-shop', 'Mobile app', 'Consulting', 'Other'];
const budgets = ['Under \$5k', '\$5k–20k', '\$20k–50k', 'Over \$50k'];
class QuoteScreen extends StatefulWidget {
const QuoteScreen({super.key});
@override
State<QuoteScreen> createState() => _QuoteScreenState();
}
class _QuoteScreenState extends State<QuoteScreen> {
final _name = TextEditingController();
final _email = TextEditingController();
final _details = TextEditingController();
String _service = services.first;
String? _budget;
DateTime? _deadline;
TurnstileSite? _site;
String? _token;
int _resetId = 0;
String _info = '';
@override
void initState() {
super.initState();
Turnstile.site(client: frontmail).then((site) => setState(() => _site = site));
}
Future<void> _pickDeadline() async {
final now = DateTime.now();
final picked = await showDatePicker(context: context, firstDate: now, lastDate: DateTime(now.year + 2));
if (picked != null) setState(() => _deadline = picked);
}
Future<void> _send() async {
final site = _site, token = _token;
if (site == null || token == null) return;
setState(() {
_token = null; // a token works only once
_resetId++;
});
try {
final deadline = _deadline;
await frontmail.send(
templateId: 'tpl_…',
params: {
'name': _name.text,
'email': _email.text,
'service': _service,
'budget': _budget,
'deadline': deadline == null ? null : deadline.toIso8601String().substring(0, 10),
'details': _details.text,
},
options: SendOptions(turnstileToken: token, turnstileKey: site.turnstileKey),
);
setState(() => _info = 'Thanks! We’ve emailed you a confirmation.');
} on FrontmailException catch (e) {
setState(() => _info = e.message);
}
}
@override
Widget build(BuildContext context) {
final site = _site;
return Scaffold(
appBar: AppBar(title: const Text('Request a quote')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
TextField(controller: _name, decoration: const InputDecoration(labelText: 'Name')),
TextField(controller: _email, decoration: const InputDecoration(labelText: 'Email'), keyboardType: TextInputType.emailAddress),
DropdownButtonFormField<String>(
value: _service,
decoration: const InputDecoration(labelText: 'What do you need?'),
items: [for (final s in services) DropdownMenuItem(value: s, child: Text(s))],
onChanged: (v) => setState(() => _service = v ?? services.first),
),
DropdownButtonFormField<String>(
value: _budget,
decoration: const InputDecoration(labelText: 'Budget (optional)'),
items: [for (final b in budgets) DropdownMenuItem(value: b, child: Text(b))],
onChanged: (v) => setState(() => _budget = v),
),
ListTile(
contentPadding: EdgeInsets.zero,
title: Text(_deadline == null ? 'Deadline (optional)' : 'Deadline: ${MaterialLocalizations.of(context).formatMediumDate(_deadline!)}'),
trailing: const Icon(Icons.calendar_today),
onTap: _pickDeadline,
),
TextField(controller: _details, decoration: const InputDecoration(labelText: 'Project details'), maxLines: 5),
const SizedBox(height: 16),
if (site != null)
TurnstileView(
site: site,
resetId: _resetId,
onMessage: (m) => setState(() => _token = m is TurnstileToken ? m.token : null),
),
FilledButton(onPressed: _token == null ? null : _send, child: const Text('Request a quote')),
Text(_info),
],
),
);
}
}
null-Werte (kein Budget, kein Termin) gelten als leer, die entsprechenden Zeilen erscheinen also
gar nicht in der E-Mail. Der Termin wird als YYYY-MM-DD gesendet; in der E-Mail steht er als
formatiertes Datum.
5. Testen
Senden Sie eine Anfrage mit Ihrer eigenen Adresse als Kunden-E-Mail. Sie erhalten die Anfrage und direkt danach die Bestätigung. Beide stehen im Verlauf.
Jede E-Mail kostet einen Credit – eine Anfrage mit Bestätigung also zwei. Credits in einer arbeitsreichen Woche aufgebraucht? Die Anfragen warten in der Warteschlange zurückgehaltener E-Mails und gehen raus, sobald Sie aufladen; der Status im Ergebnis ist
held.
Nächste Schritte
- Aktivieren Sie Kontakte sammeln in den Einstellungen der Vorlage, um eine Liste aller
Interessenten zu führen (
email,nameundcompanysind bereits zugeordnet). - Senden Sie
SendOptions(locale: Localizations.localeOf(context).toLanguageTag()), um die Sprachvariante der Vorlage für die Sprache des Nutzers zu verwenden.


