Zum Inhalt

Beispielarbeiten · Software & Web-Apps

Flutter: Aufgaben-App mit Formular, Prüfung und lokaler Speicherung

Eine kleine App mit Liste, Formular, Abhaken, Löschen und lokaler Speicherung. Analyse ohne Meldung, Unit- und Widget-Tests grün, als Web-App gebaut und im Browser gerendert.

Musterarbeit (Demo), kein Kundenauftrag. Gebaut als Probeaufgabe mit erfundenen Beispieldaten; die Prüfzahlen unten stammen aus dem automatischen Prüflauf und sind nicht geschönt.

Was gebaut wurde

  • Liste, Formular mit Eingabeprüfung, Abhaken und Löschen
  • Doppelte und zu kurze Titel werden abgelehnt, die Liste bleibt nach dem Neustart erhalten
  • Unit- und Widget-Tests, Analyse ohne Meldung
  • Gegenprobe: absichtlich kaputte Fassungen werden erkannt

Bildschirmfoto aus dem Prüflauf: Flutter: Aufgaben-App mit Formular, Prüfung und lokaler Speicherung

tests.txt herunterladen flutter-app-quellcode.zip herunterladen

Gebaut und gerendert nur als Web-App. Ein Android- oder iOS-Paket ist nicht entstanden, es lief nichts auf einem Gerät oder Emulator; Kamera, GPS, Push und Berechtigungen sind nicht geprüft. Keine Store-Veröffentlichung, keine Signierung, keine Anbindung an Firebase oder ein Backend. Zustand nur mit setState.

Wie geprüft wurde

Automatischer Prüflauf vom 01.11.2026, aus dem Protokoll (Testdaten erfunden, Zahlen gemessen):

  • flutter analyze: keine Meldung
  • flutter test: 13 Tests (Unit + Widget) gruen
  • flutter build web --release: ok, main.dart.js 2388 KB
  • Chrome headless: Seitentext ok, Navigation zum Formular ok, Bild 480x800, Textpixel 1578, Markenfarbe 8791
  • Gegenprobe 3/3 kaputte Fassungen erkannt

Grenzen

Was diese Probe nicht abdeckt, steht hier, damit niemand mehr erwartet:

  • Flutter 3.47.9 / Dart 3.13.7 getestet; nur Web-Ziel gebaut und im Browser (Chrome headless) gerendert; Android-APK/AAB und iOS-IPA nicht gebaut (kein Android SDK/Android Studio, kein Mac/Xcode) …
  • kein Lauf auf echtem Geraet oder Emulator: Touch-Gesten, Tastatur, Kamera, GPS, Push, Sensoren, plattformspezifischer Code (Kotlin/Swift, Method Channels), Berechtigungen nicht geprueft
  • keine Store-Veroeffentlichung (Google Play, App Store), keine Signierung/Zertifikate/Bundle-IDs; Firebase, Backend-/REST-Anbindung, Login, In-App-Kaeufe, Zahlungen nicht geprueft
  • Speicherung nur lokal (shared_preferences); State-Management nur mit setState, kein Provider/Riverpod/Bloc; Integrationstests (integration_test) und Golden-Tests nicht Teil der Probe
  • Web-Rendering mit Software-Grafik; Leistung, Ladezeit, Barrierefreiheit und andere Browser (Safari, Firefox) nicht gemessen; Pakete aus pub.dev ausser shared_preferences nicht geprueft

Die Tests

import 'package:aufgaben_app/aufgaben.dart';
import 'package:aufgaben_app/main.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';

void main() {
  setUp(() => SharedPreferences.setMockInitialValues({}));

  group('Pruefung des Titels', () {
    test('leer wird abgelehnt', () {
      expect(pruefeTitel('', []), 'Bitte einen Titel eingeben');
      expect(pruefeTitel('   ', []), 'Bitte einen Titel eingeben');
      expect(pruefeTitel(null, []), 'Bitte einen Titel eingeben');
    });
    test('zu kurz und zu lang', () {
      expect(pruefeTitel('ab', []), 'Mindestens 3 Zeichen');
      expect(pruefeTitel('x' * 41, []), 'Hoechstens 40 Zeichen');
      expect(pruefeTitel('x' * 40, []), isNull);
    });
    test('Doppelte werden abgelehnt, egal wie geschrieben', () {
      final v = [Aufgabe(titel: 'Einkaufen')];
      expect(pruefeTitel('einkaufen ', v), 'Diese Aufgabe gibt es schon');
      expect(pruefeTitel('Waesche', v), isNull);
    });
  });

  group('Liste und Speicherung', () {
    test('hinzufuegen, umschalten, zaehlen, entfernen', () {
      final l = AufgabenListe();
      l.hinzufuegen('  Eins ', 'a');
      l.hinzufuegen('Zwei', '');
      expect(l.eintraege.first.titel, 'Eins');
      expect(l.offen, 2);
      l.umschalten(0);
      expect(l.offen, 1);
      expect(l.eintraege[0].erledigt, isTrue);
      l.entfernen(0);
      expect(l.eintraege.length, 1);
      expect(l.eintraege.single.titel, 'Zwei');
    });
    test('Speichern und Laden ergibt dieselbe Liste', () async {
      final l = AufgabenListe();
      l.hinzufuegen('Steuer', 'bis Freitag');
      l.hinzufuegen('Sport', '');
      l.umschalten(1);
      await l.speichern();
      final neu = await AufgabenListe.laden();
      expect(neu.eintraege.map((a) => a.titel), ['Steuer', 'Sport']);
      expect(neu.eintraege[0].notiz, 'bis Freitag');
      expect(neu.eintraege[1].erledigt, isTrue);
      expect(neu.offen, 1);
    });
    test('ohne gespeicherte Daten ist die Liste leer', () async {
      expect((await AufgabenListe.laden()).eintraege, isEmpty);
    });
  });

  group('Bedienung', () {
    Future<void> neueAufgabe(WidgetTester t, String titel,
        [String notiz = '']) async {
      await t.tap(find.byKey(const Key('neu')));
      await t.pumpAndSettle();
      await t.enterText(find.byKey(const Key('feld-titel')), titel);
      if (notiz.isNotEmpty) {
        await t.enterText(find.byKey(const Key('feld-notiz')), notiz);
      }
      await t.tap(find.byKey(const Key('speichern')));
      await t.pumpAndSettle();
    }

    testWidgets('Start zeigt leere Liste', (t) async {
      await t.pumpWidget(const AufgabenApp());
      await t.pumpAndSettle();
      expect(find.text('Meine Aufgaben'), findsOneWidget);
      expect(find.byKey(const Key('leer')), findsOneWidget);
      expect(find.text('0 offen'), findsOneWidget);
    });

    testWidgets('Navigation zum Formular und zurueck', (t) async {
      await t.pumpWidget(const AufgabenApp());
      await t.pumpAndSettle();
      await t.tap(find.byKey(const Key('neu')));
      await t.pumpAndSettle();
      expect(find.text('Neue Aufgabe'), findsWidgets);
      expect(find.byKey(const Key('feld-titel')), findsOneWidget);
      await t.pageBack();
      await t.pumpAndSettle();
      expect(find.text('Meine Aufgaben'), findsOneWidget);
      expect(find.byKey(const Key('feld-titel')), findsNothing);
    });

    testWidgets('Validierung zeigt Fehler und bleibt im Formular', (t) async {
      await t.pumpWidget(const AufgabenApp());
      await t.pumpAndSettle();
      await t.tap(find.byKey(const Key('neu')));
      await t.pumpAndSettle();
      await t.tap(find.byKey(const Key('speichern')));
      await t.pumpAndSettle();
      expect(find.text('Bitte einen Titel eingeben'), findsOneWidget);
      await t.enterText(find.byKey(const Key('feld-titel')), 'ab');
      await t.tap(find.byKey(const Key('speichern')));
      await t.pumpAndSettle();
      expect(find.text('Mindestens 3 Zeichen'), findsOneWidget);
      expect(find.byKey(const Key('feld-titel')), findsOneWidget);
    });

    testWidgets('Eingabe legt Aufgabe an und speichert sie', (t) async {
      await t.pumpWidget(const AufgabenApp());
      await t.pumpAndSettle();
      await neueAufgabe(t, 'Rechnung schreiben', 'fuer Kunde A');
      expect(find.text('Rechnung schreiben'), findsOneWidget);
      expect(find.text('fuer Kunde A'), findsOneWidget);
      expect(find.text('1 offen'), findsOneWidget);
      expect(find.byKey(const Key('leer')), findsNothing);
      final gespeichert = await AufgabenListe.laden();
      expect(gespeichert.eintraege.single.titel, 'Rechnung schreiben');
    });

    testWidgets('Doppelte Aufgabe wird im Formular abgelehnt', (t) async {
      await t.pumpWidget(const AufgabenApp());
      await t.pumpAndSettle();
      await neueAufgabe(t, 'Putzen');
      await t.tap(find.byKey(const Key('neu')));
      await t.pumpAndSettle();
      await t.enterText(find.byKey(const Key('feld-titel')), 'putzen');
      await t.tap(find.byKey(const Key('speichern')));
      await t.pumpAndSettle();
      expect(find.text('Diese Aufgabe gibt es schon'), findsOneWidget);
    });

    testWidgets('Abhaken und Loeschen wirken und werden gespeichert',
        (t) async {
      await t.pumpWidget(const AufgabenApp());
      await t.pumpAndSettle();
      await neueAufgabe(t, 'Anruf');
      await neueAufgabe(t, 'Mail');
      expect(find.text('2 offen'), findsOneWidget);
      await t.tap(find.byType(Checkbox).first);
      await t.pumpAndSettle();
      expect(find.text('1 offen'), findsOneWidget);
      expect((await AufgabenListe.laden()).eintraege.first.erledigt, isTrue);
      await t.tap(find.byIcon(Icons.delete_outline).first);
      await t.pumpAndSettle();
      expect(find.text('Anruf'), findsNothing);
      expect(find.text('Mail'), findsOneWidget);
      expect((await AufgabenListe.laden()).eintraege.length, 1);
    });

    testWidgets('Gespeicherte Aufgaben erscheinen beim Neustart', (t) async {
      SharedPreferences.setMockInitialValues({
        AufgabenListe.schluessel:
            '[{"titel":"Altlast","notiz":"","erledigt":false}]',
      });
      await t.pumpWidget(const AufgabenApp());
      await t.pumpAndSettle();
      expect(find.text('Altlast'), findsOneWidget);
      expect(find.text('1 offen'), findsOneWidget);
    });
  });
}

So etwas für deinen Betrieb?

Beschreib kurz, was du brauchst. Du bekommst einen Festpreis, bevor ich anfange; ohne dein Ja passiert nichts.

Zur Werkstatt