Zum Inhalt springen

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:

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

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

  1. E-Mail-Dienste → Dienst hinzufügen: Verbinden Sie Ihr Postfach oder Ihren Anbieter, aktivieren Sie Als Standard festlegen, dann Prüfen & speichern.
  2. Vorlagen → Aus der Galerie → Angebotsanfrage (Kategorie Geschäftlich) → Vorlage verwenden.
  3. Bestätigung hinzufügen: Aus der Galerie → Automatische Antwort: Danke für Ihre Nachricht → Vorlage verwenden, auf ihrem Tab Parameter Standardwerte für company_name und response_time festlegen 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-Origin mit origin_not_allowed abgelehnt.
  • 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, name und company sind bereits zugeordnet).
  • Senden Sie SendOptions(locale: Localizations.localeOf(context).toLanguageTag()), um die Sprachvariante der Vorlage für die Sprache des Nutzers zu verwenden.
Alle Tutorials