Skip to content

Repository files navigation

Loopky

A mobile flashcards app that fuses Duolingo TinyCards' playfulness, Anki's spaced repetition, and Pubky's decentralized identity and social graph. iOS + Android, built with Kotlin Multiplatform.

There is no Loopky account and no Loopky server. You sign in with a key held by Pubky Ring, and your decks and study progress are written to a Pubky homeserver you hold the key to.

  • Architecture: docs/Architecture.md — module layout, layering, homeserver data model, Pubky/Nexus integration, what is still open. Start here.
  • Import spec: docs/specs.md — the paste-to-import flow and the parser rules the test suite is written against.
  • Privacy: PRIVACY.md — what reaches a homeserver, and whose it is.

Screenshots

Captured against a live Pubky homeserver on both platforms. Full-resolution files live in screenshots/ and double as the store asset sets.

Android

Captured on the v0.6.0 debug build — a Pixel phone emulator (1080×2400) and the Pixel_Tablet emulator in landscape (2560×1600), the two width classes the adaptive layouts target.

Phone

Onboarding
Sign in
A key, not an account
Today
Today
The daily queue
Deck library
Library
Your decks
Deck detail
Deck
Stats, tags, cards
Study prompt
Study
Listen and Speak
Grading a card
Grade
The button says the interval
Discover
Discover
Decks and tags
Profile
Profile
Your Pubky identity
Paste to import
Paste import
Separator auto-detected
Homeserver signup
Signup
SMS, sats or invite

Tablet

Home, deck detail and onboarding are real two-pane layouts at expanded width, with a navigation rail in place of the tab bar — not a stretched phone column.

Today on tablet
Today — queue beside the deck grid
Deck library on tablet
Library — four columns at expanded width
Deck detail on tablet
Deck — metadata beside the full card list
Study on tablet
Study — the prompt side
Grading on tablet
Grade — grades move beside the card
Discover on tablet
Discover — trending tags and follows
Profile on tablet
Profile — identity and totals
Onboarding on tablet
Sign in — split across two panes

Settings and the recovery-phrase screens are FLAG_SECURE, so they cannot be screenshotted — that is deliberate, and why they are absent here.

iOS

Captured on the Release build (production Nexus + Homegate) against a live homeserver, on the iPhone 17 Pro Max simulator (1320×2868) and the iPad Pro 13-inch simulator (2064×2752) — the two sizes App Store Connect asks for. The SwiftUI screens are native, not Compose Multiplatform, and the iPad has its own regular-size-class layouts rather than a stretched phone column.

iPhone

Today
Today
The daily queue
Deck library
Library
Your decks
Deck detail
Deck
Stats, tags, cards
Study prompt
Study
The prompt side
Grading a card
Grade
Listen, Speak, and the interval on the button
Discover
Discover
Decks and tags from people you follow
Profile
Profile
Your Pubky identity

iPad

Today on iPad
Today — queue beside the deck grid
Deck library on iPad
Library — multi-column at regular width
Study on iPad
Study — the prompt side
Grading on iPad
Grade — grades move beside the card
Discover on iPad
Discover — four columns of decks and tags
Profile on iPad
Profile — identity and totals
Onboarding on iPad
Sign in — a simulator has no Pubky Ring, so the QR handoff is raised automatically

Status

Android is feature-built end to end. Onboarding and Pubky Ring sign-in, homeserver signup (SMS / Lightning / invite code), the daily study queue, the SRS study loop with Listen / Speak / Type, deck library and editor, paste import and bulk file import (.txt / .csv / .apkg), publishing, discovery and tag browse, profiles and follows, and settings.

iOS is at feature parity. Browse without an account, then sign in with Pubky Ring — by deeplink, or by scanning a QR from the phone that holds your key — or create an account on a homeserver (SMS, Lightning or invite code), restore one from a recovery phrase or file, and back a key up three ways. Then the daily study queue, the SRS study loop with Listen / Speak / Type and reverse cards, the deck library and editor, paste import and bulk file import (.txt / .csv / .apkg), publishing, discovery and tag browse, profiles and follows, and settings including the synced study intervals. Driven against a real homeserver on the iPhone 17 simulator; see the iOS sections of journeys/RESULTS.md, which also record what could not be reached there and why.

iPad and every width but a phone is #173.

Roughly 680 shared tests run on every PR. Two known blockers before a Play release: the native Pubky libraries fail 16 KB page-size alignment, and release packaging is still an unsigned universal APK with no R8.


Architecture at a glance

Business logic is shared; UI is native per platform. That is the one rule to internalize.

  • shared/ — KMP module holding domain models, repositories (which own the business logic — there is no use-case layer), and the ViewModels both platforms consume. Platform glue (Pubky FFI, TTS, speech recognition, background work) is either expect/actual or a Koin-bound interface.
  • composeApp/ — the Android app. Jetpack Compose screens, Navigation Compose, Koin. Android-only despite the name; Compose Multiplatform UI is not used for iOS.
  • iosApp/ — the iOS app. SwiftUI screens, NavigationStack, Koin bootstrap.

Pubky is reached through one interface, PubkyClient, over the UniFFI bindings generated by pubky-core-ffi-fork and checked in. Published decks live on the author's homeserver as a manifest plus chunked card records; the homeserver is the source of truth and the app is not offline-first. Global questions a single homeserver cannot answer — trending tags, search, "who else uses Loopky" — go to the Pubky Nexus indexer.

Module layout

loopky/
├── shared/
│   └── src/
│       ├── commonMain/kotlin/com/github/jvsena42/loopky/
│       │   ├── domain/        # models (pure Kotlin, no framework imports)
│       │   ├── data/          # repositories, PubkyClient, Nexus, storage
│       │   └── presentation/  # ViewModels (StateFlow + SharedFlow)
│       ├── commonTest/        # the whole automated suite
│       ├── androidMain/       # Pubky FFI, TTS, speech, WorkManager, Koin
│       └── iosMain/           # Pubky adapter, TTS, speech, BGTaskScheduler, Koin
│
├── composeApp/src/androidMain/kotlin/com/github/jvsena42/loopky/
│   ├── ui/                    # Compose screens + navigation
│   ├── LoopkyApp.kt           # Application; starts Koin
│   └── MainActivity.kt
│
├── iosApp/iosApp/
│   ├── Views/                 # SwiftUI screens
│   ├── Navigation/            # NavigationStack
│   ├── DI/                    # Koin bootstrap + Flow→SwiftUI bridge
│   ├── Pubky/                 # IosPubkyClient + generated bindings
│   └── iOSApp.swift
│
└── journeys/                  # scripted end-to-end journeys + results

Stack

Concern Choice
UI (Android) Jetpack Compose + Material 3 Expressive
UI (iOS) SwiftUI + NavigationStack
Shared logic Kotlin Multiplatform (commonMain)
DI Koin
Async Coroutines + Flow; hand-rolled Swift bridge (IosFlowWatcher / FlowObserver)
Persistence Pubky homeserver + in-memory session cache — no local database
Secrets Liftric KVault → Android Keystore / iOS Keychain
Identity / social Pubky (pubky-core-ffi-fork, UniFFI) + Nexus indexer for global reads
Navigation Per-platform native
Lint detekt (Kotlin) · SwiftLint (Swift)

Android minSdk 29, targetSdk 36.


Build and run

Android

./gradlew :composeApp:assembleDebug

Or use the run configuration from your IDE's toolbar.

iOS

Open /iosApp in Xcode and run. shared is consumed as a static framework.

Tests and checks

./gradlew :shared:allTests              # shared KMP tests (~680)
./gradlew :shared:compileKotlinMetadata # fast commonMain compile check
./gradlew detektAll                     # Kotlin lint (add --auto-correct to fix formatting)
./gradlew lintSwift                     # Swift lint (needs `brew install swiftlint`)

CI runs detekt, the unit tests, and an Android debug build on every PR.

End-to-end coverage is manual and scripted: journeys/ holds 19 numbered journeys driven on a device with android-cli, with results and dates in journeys/RESULTS.md. A green build says nothing about what the screen renders.


Learn more about Kotlin Multiplatform.

About

Learn anything, remember everything

Topics

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages