Zum Inhalt springen
Griff Keeper
Mobile App

Griff Keeper

Eine private App für Abos, Versicherungen und Fristen — ohne Konto, ohne Cloud.

Verfügbar bei Google Play ·

  • Android
  • Kotlin
  • Jetpack Compose
  • Room
  • Offline-first
  • Clean Architecture
Kurzbeschreibung

Abos, Versicherungen und Zahlungsfristen. Offline. Ohne Konto.

Screenshots

Ziehen Sie die Leiste zur Seite, um zu blättern. Klicken Sie auf einen Screenshot, um ihn zu vergrößern, und wechseln Sie dann mit den Pfeiltasten.

Vollständige Beschreibung

Griff Keeper ist eine datenschutzfreundliche App für Abonnements, Versicherungen, wiederkehrende Zahlungen und Fristen — ohne Konto, ohne Backend, ohne Firebase und ohne Cloud-Synchronisierung. Alles bleibt auf Ihrem Telefon. Der einzige Weg, auf dem Daten das Gerät verlassen, ist eine verschlüsselte Backup-Datei, die Sie selbst erstellen und selbst verschieben.

WAS AN EINEM ORT GESAMMELT WIRD

Eine Netflix-Verlängerung, eine im September auslaufende Kfz-Versicherung, eine Grundsteuerfrist, eine Hosting-Verlängerung — und die App sagt Ihnen Bescheid, bevor jede davon wichtig wird.

ABONNEMENTS

Eine Liste mit dem Markenzeichen des Anbieters, Kategorie, Abrechnungszeitraum und Preis. Monatliche und jährliche Summen vermischen nie die Zeiträume — ein Jahresabo zählt mit seinem echten Jahresbetrag, nie mit dem Monatspreis mal zwölf. Ein Offline-Katalog von 52 beliebten Diensten mit Standard-Verwaltungslinks, plus ein eigener Eintrag für alles andere.

VERSICHERUNGEN & VERPFLICHTUNGEN

Kfz-, Wohngebäude-, Grundstücks- und Drohnenversicherung, Grundsteuer, Grunderwerbsteuer — jeder Eintrag hält Zahlungsdatum, Ablaufdatum und Fälligkeitsdatum auseinander, weil sie drei verschiedene Fragen beantworten. Die Liste sortiert nach der Frist, die wirklich zählt, die dringendste — auch überfällige — zuerst.

ERINNERUNGEN OHNE NETZWERKVERBINDUNG

Lokale Benachrichtigungen auf Basis von WorkManager — kein Firebase, kein Netzwerkzugriff jeglicher Art. Versicherungen erinnern 30, 7 und 1 Tag vor Ablauf; Verpflichtungen und Abos 7 und 1 Tag vor der Frist. Eine bezahlte Police erinnert weiterhin, wenn der Versicherungsschutz ausläuft, denn genau das ist es wert, gewusst zu werden.

STATISTIKEN, DIE NICHTS VERMISCHEN

Geschätzte Abo-Kosten und eine tatsächlich geleistete Zahlung sind zwei verschiedene Arten von Zahlen — nebeneinander angezeigt, nie addiert, ohne zu sagen, welcher Teil eine Schätzung ist.

IHRE DATEN VERLASSEN DAS GERÄT NIE VON ALLEIN

Der Export erzeugt ein verschlüsseltes Backup, geschützt durch ein Passwort, das nur Sie wählen — die App sieht es nie und kann es nicht wiederherstellen. Der Import zeigt eine Vorschau des Backup-Inhalts, bevor irgendetwas geschrieben wird, und beim Zusammenführen wird ein Eintrag nie stillschweigend durch eine ältere Version überschrieben.

SPRACHE UND WÄHRUNG

Vollständige polnische und englische Oberfläche, zur Laufzeit umschaltbar. PLN und EUR, unabhängig von der Sprache, mit Umrechnung bestehender Beträge zu einem Kurs, den Sie selbst angeben — es wird niemals ein Wechselkursdienst kontaktiert.

KEINE WERBUNG. KEIN KONTO. KEIN TRACKING.

Die App hat keine Internetberechtigung. Sie könnte nichts senden, selbst wenn sie es wollte.


Technologie

Leistungsumfang: Produktdesign, Architektur, Implementierung, Deployment

Mobile Anwendung
  • Kotlin
  • Jetpack Compose
  • Material 3
  • Navigation Compose
  • Hilt
  • Coroutines
  • Flow / StateFlow
  • WorkManager
  • DataStore Preferences
  • AppCompat (per-app locales)
  • Gradle Kotlin DSL
  • KSP
Daten auf dem Gerät
  • Room
  • SQLite
  • javax.crypto (AES-256-GCM, PBKDF2)
  • Android Storage Access Framework
Tests
  • JUnit
  • Espresso
  • Compose UI Test
  • Instrumented Room migration tests
Auslieferung
  • Google Play

Fallstudie — Architektur und Technologie

11 Min.

Das Projekt in einem Absatz

Griff Keeper ist eine native Android-App zum Verfolgen von Abonnements, Versicherungen und wiederkehrenden Verpflichtungen, gebaut auf der entgegengesetzten Annahme zu Griff Gym: hier gibt es kein Backend, kein Konto und überhaupt kein Netzwerk. Jedes Stück Logik und jeder Teil des Datenmodells folgt Clean Architecture als echte Gradle-Module — dieselbe Disziplin wie bei Griff Gym, angewendet auf ein Produkt, das absichtlich nie mit dem Internet spricht.

Die Annahme, die eine ganze Fehlerklasse beseitigt

Eine Finanz-App, die Versicherungs- und Zahlungsdaten hält, hat eine offensichtliche architektonische Weggabelung: ein Backend mit Konto, oder nichts. Griff Keeper wählt nichts — und das ist kein fehlendes Feature, sondern eine bewusste Produktentscheidung. Ein Backend ist Angriffsfläche, Wartungsaufwand und eine Stelle, an der Nutzerdaten austreten können, ohne dass jemand es merkt. Eine App ohne INTERNET-Berechtigung im Manifest kann das nicht, selbst wenn jemand in sie einbräche.

Die Konsequenz ist leicht zu formulieren und später schwer rückgängig zu machen: der einzige Weg, auf dem Daten das Telefon verlassen, ist eine Datei, die der Nutzer selbst erstellt und selbst verschiebt. Import/Export hört auf, ein Nebenfeature zu sein, und wird zu einem zentralen Teil des Produkts.

Architektur — dieselben Grenzen wie bei Griff Gym, ein anderer Ausgangspunkt

Clean Architecture als echte Gradle-Module, Abhängigkeiten zeigen nach innen:

:app             Composition Root — Application, MainActivity, Hilt-Bootstrap
:presentation    Compose-UI, ViewModels, typsichere Navigation, Material-3-Theme
:infrastructure  Room, DataStore, WorkManager, Backup-Codec
:application     Use Cases
:domain          Entitäten, Value Objects, Ports, Geschäftsregeln

Umkehrung der Abhängigkeit in einem Satz: :infrastructure hängt von :domain ab und implementiert dessen Ports — SubscriptionRepository, ObligationRepository, ReminderPublisher, BackupCodec, ClockProvider und die Id-Generatoren — und :domain/:application wissen nicht, dass Room, WorkManager oder javax.crypto überhaupt existieren. :app ist das einzige Modul, das Implementierungen über Hilt mit Ports verdrahtet, und das einzige, das die BuildConfig der App liest.

Daten fließen in eine Richtung, in beide Richtungen:

UI-Ereignis -> ViewModel -> UseCase -> Repository -> Room
Room-Flow -> Repository -> UseCase -> ViewModel-StateFlow -> Compose

Bildschirme gliedern sich in Route (Hilt und Zustandssammlung), Screen (Scaffold, Snackbars, Dialoge) und Content (zustandsloses Layout) — genau dieselbe Aufteilung wie bei Griff Gym —, wodurch die Compose-Schicht ohne Hilt testbar bleibt.

Geld ist nie eine Gleitkommazahl

Money ist eine Value-Klasse über Long-Kleinsteinheiten (Groschen, Cent), mit plus, times und einem kaufmännisch gerundeten dividedBy, plus einer Nicht-negativ-Invariante. Die Anzeigeformatierung — 34,99 zł, 1.299,00 PLN oder 34,99 € — geschieht erst an der UI-Grenze, über die CLDR-Daten des aktiven Locale. Der gespeicherte Betrag ändert sich nie von selbst; nur eine bewusste, vom Nutzer bestätigte Währungsumrechnung schreibt ihn um.

Auch die Kostennormalisierung lebt in der Domäne: monthlyEquivalent teilt einen Jahrespreis durch 12 (kaufmännisch gerundet), yearlyEquivalent multipliziert einen Monatspreis mit 12, und die Jahressumme wird aus echten Jahresbeträgen gebildet — nie als Monatssumme × 12, was Jahresabos durch monatliche Rundung verzerren würde.

Abonnements und Verpflichtungen sind bewusst zwei getrennte Modelle

Sie wurden nicht zu einer Expense-Entität zusammengelegt, weil sie unterschiedliche Fragen beantworten: ein Abo ist ein wiederkehrender Preis mit Abrechnungszeitraum, eine Verpflichtung ist ein einzelner Betrag mit Zahlungsstatus und bis zu drei unterschiedlichen Daten. Das Modell hält sie bewusst auseinander:

FeldBedeutung
Zahlungsdatumdas einzige Datum, dem eine Ausgabe zugeordnet wird — eine im Dezember 2026 bezahlte, bis Dezember 2027 gültige Police ist eine Ausgabe des Jahres 2026
gültig bissteuert Ablauferinnerungen, nie die Ausgabenzuordnung
Fälligkeitsdatumdie Frist einer offenen, unbezahlten Forderung

Das ist keine kosmetische Unterscheidung. Diese drei Daten in Statistiken oder Erinnerungen zu vermischen, gäbe dem Nutzer ein falsches Bild davon, was er in einem bestimmten Jahr tatsächlich ausgegeben hat, und würde ihn zur falschen Zeit an die falsche Sache erinnern.

Erinnerungen ohne ein einziges über das Netz gesendetes Byte

Ein täglicher WorkManager-Job fragt die Domäne, was heute bedeutet, und stellt Android-Benachrichtigungen über NotificationCompat zu. Es gibt kein Firebase Cloud Messaging und keinerlei Netzwerkzugriff. WorkManager übersteht bereits einen Neustart und ein erzwungenes Beenden und respektiert Doze, sodass die App keinen eigenen BOOT_COMPLETED-Receiver braucht.

Standardplan:

Versicherungen         30, 7 und 1 Tag vor Ablauf
Andere Verpflichtungen  7 und 1 Tag vor Fälligkeit
Abonnements             7 und 1 Tag vor Verlängerung

Eine beglichene Forderung erzeugt keine Zahlungserinnerungen mehr, aber eine bezahlte Versicherungspolice erzeugt weiterhin Ablauferinnerungen — dass der Versicherungsschutz ausläuft, ist die Sache, die es wert ist, gewusst zu werden, unabhängig davon, ob die Rechnung bereits beglichen ist.

Backup als einzige Brücke zur Außenwelt

Ohne Backend hört das Verschieben von Daten zwischen Geräten auf, ein Nebenfeature zu sein. Ein Backup ist ein logisches Dokument, keine Kopie der SQLite-Datei:

Room + DataStore
        ↓
portables Backup-Modell (Domänenobjekte)
        ↓
JSON  →  GZIP  →  AES-256-GCM
        ↓
verschlüsselte .griffbackup-Datei

Das Format hängt nicht von der Oberflächensprache ab — Kategorien, Zahlungsstatus und Währungen werden als stabile Bezeichner geschrieben (MUSIC, VEHICLE_INSURANCE, PLN), nie als die Wörter, die ein Bildschirm gerade anzeigt. Ein von der polnischen App geschriebenes Backup importiert in die englische und umgekehrt.

Verschlüsselung ohne fest eingebauten Schlüssel

  • PBKDF2WithHmacSHA256, 210.000 Iterationen, ein frisches 128-Bit-Salt pro Backup, 256-Bit-Ausgabe. Die Iterationszahl wird in die Datei geschrieben, sodass eine spätere Erhöhung bestehende Backups nicht verwaist.

  • AES-256-GCM, ein frischer 96-Bit-Nonce pro Backup, 128-Bit-Tag. Authentifizierte Verschlüsselung macht „diese Datei wurde verändert" zu einem erkennbaren Zustand statt zu einer Entschlüsselung, die plausibel aussehendes Rauschen zurückgibt.

  • Es gibt keinen fest eingebauten Schlüssel in der App. Eine Konstante in der APK, in der BuildConfig oder in einem Asset ist für jeden wiederherstellbar, der den Download entpackt, und ein Android-Keystore-Schlüssel ist an das Gerät gebunden, das ihn erzeugt hat — genau die Eigenschaft, die ein Backup nicht haben darf. Das einzige Schlüsselmaterial stammt aus dem vom Nutzer eingegebenen Passwort, und die App kann ein vergessenes Passwort nicht wiederherstellen. Das ist eine bewusste Folge des Offline-Seins.

Vier Schranken, bevor auch nur eine Zeile geschrieben wird

  1. Format — eine Größenobergrenze, ein magischer Marker, grenzgeprüftes Header-Parsing. Alles, was nicht passt, wird als „nicht unsere Datei" abgelehnt.

  2. Authentifizierung — AES-GCM prüft den Tag, bevor irgendein Klartext zurückgegeben wird, sodass eine teilweise entschlüsselte Nutzlast nie beim Importer ankommt. Ein falsches Passwort und eine veränderte Datei werden als eine Kategorie gemeldet, weil GCM sie tatsächlich nicht unterscheiden kann.

  3. Schemaversion — eine Datei aus einem neueren Build oder aus einem Schema, das dieser Build nicht mehr erreicht, wird ausdrücklich abgelehnt, statt optimistisch geparst zu werden.

  4. Domänenvalidierung — jeder Datensatz durchläuft dieselben Grenzen wie ein von Hand eingegebener Datensatz. Alles oder nichts: Ein Backup ist ein Dokument, und fehlgeschlagene Datensätze stillschweigend fallen zu lassen, würde etwas wiederherstellen, das der Nutzer nie hatte.

Importierte Datensätze landen in einer einzigen Room-Transaktion, sodass ein halb angewendeter Import kein erreichbarer Zustand ist.

Import mit Merge-Modus und Replace-Modus

MERGE behält lokale Datensätze und ergänzt, was das Backup mitbringt — ein auf beiden Seiten bekannter Datensatz bleibt unverändert, wenn er identisch ist, und wird aktualisiert, wenn updatedAt des Backups neuer ist; bei Gleichstand bleibt die lokale Zeile bestehen. REPLACE verwirft lokale portable Daten und hinterlässt genau das, was das Backup mitbrachte — es erfordert eine zusätzliche Bestätigung.

Identität ist die eigene UUID des Datensatzes, einmal bei der Erstellung vergeben und nie neu geschrieben, sodass das zweimalige Importieren derselben Datei ein No-op ist statt einer Quelle für Duplikate. Datensätze, die nur ähnlich aussehen (gleicher Name, andere Id), werden in der Vorschau gezählt und gemeldet, nie automatisch zusammengeführt — nur die Id gilt als Beweis.

Eine fehlende Berechtigung als bewusstes Produktmerkmal

Die gesamte Berechtigungsliste der App hat zwei Einträge: lokale Benachrichtigungen und eine lesende Netzwerkstatus-Prüfung, die für genau eine Sache existiert — die Warnung, dass ein geteiltes Backup in einem Postausgang liegen bleiben könnte. Es gibt keine `INTERNET`-Berechtigung. Es gibt auch keine Speicherberechtigung jeglicher Art — Import und Export laufen über die System-Dateiauswahl (Storage Access Framework), sodass die App nur Zugriff auf genau die eine Datei erhält, die der Nutzer ausgewählt hat.

Technologie-Stack

BereichTechnologien
AppKotlin, Jetpack Compose, Material 3, Navigation Compose (typsichere Routen), Hilt, Coroutines, Flow/StateFlow, WorkManager, DataStore Preferences, AppCompat (App-eigene Sprachen)
Daten auf dem GerätRoom, SQLite, javax.crypto (AES-256-GCM, PBKDF2), Storage Access Framework
TestsJUnit, Espresso, Compose UI Test, instrumentierte Room-Migrationstests
AuslieferungGoogle Play

Room-Migrationen sind rein additiv — fallbackToDestructiveMigration() wird im gesamten Projekt nirgends verwendet, und jede Erhöhung der Schemaversion trägt ihren eigenen instrumentierten Migrationstest.

Was dieses Projekt tatsächlich gelehrt hat

Griff Gym und Griff Keeper sind zwei Produkte im selben architektonischen Stil, an entgegengesetzten Polen einer Entscheidung: ein Backend haben, oder nicht. Dieselbe Gradle-Modul-Disziplin, dieselbe Route/Screen/Content-Aufteilung, dieselbe Regel, die Domäne vom Framework fernzuhalten — einmal angewendet auf eine App, die mit einem Server synchronisiert, und einmal auf eine App, die absichtlich mit nichts spricht.

Das, was ich wiederholen würde: kein Backend ist immer noch eine Architektur, nicht das Fehlen einer solchen. Die Entscheidung, dass die einzige Brücke zur Außenwelt eine verschlüsselte Datei sein würde, die der Nutzer selbst erstellt und selbst verschiebt, erforderte genauso viel Überlegung wie der Entwurf einer API — sie floss nur in Kryptographie und vier Validierungsschranken statt in einen HTTP-Vertrag.


Sprechen wir über das geplante Projekt

Das Erstgespräch ist kostenlos und unverbindlich. Eine kurze Beschreibung des Prozesses und des erwarteten Ergebnisses genügt.

Sprechen wir über das Projekt