Loopky is a free, open source flashcards app with spaced repetition, and a full replacement for Anki for most learners. Import your Anki decks, study on your phone or tablet, and manage decks from your computer with the command line tool. It reads cards aloud, checks your pronunciation, and shares decks with your class through one link. Get it on Google Play · loopky.app
- Spaced repetition with Again, Hard, Good and Easy. Each button shows the interval it gives, and you can change the intervals.
- Import Anki
.apkgfiles,.txtand.csv, or paste a list straight in. - Listen, Speak and Type modes in 35 languages: hear the card, say the answer and have your pronunciation checked, or type it.
- Reverse cards and images on either side of a card.
- Two-pane tablet layouts, not a stretched phone screen.
- Share decks by link. Publish a deck, follow and copy other people's, and find new ones in Discover.
- A desktop command line tool to manage decks and cards in bulk, or to let an AI agent build
them for you — see
cli/README.md. - No ads and no analytics.
- Switch from Anki
- Loopky for language learning
- Loopky for medical students
- Loopky for teachers
- AI flashcards
- Compare Loopky with Anki and Quizlet
- FAQ
Loopky blends 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.
Captured against a live Pubky homeserver on both platforms. Full-resolution files live in
screenshots/ and double as the store asset sets.
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.
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.
Settings and the recovery-phrase screens are
FLAG_SECURE, so they cannot be screenshotted — that is deliberate, and why they are absent here.
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.
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 1,300 shared tests run on every PR.
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 eitherexpect/actualor a Koin-bound interface.androidApp/— the Android app. Jetpack Compose screens, Navigation Compose, Koin. Android-only, and a plaincom.android.applicationmodule rather than a KMP one; 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.
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
│
├── androidApp/src/main/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
│
├── cli/ # `loopky`, the headless client — a GraalVM binary on :shared's
│ # jvm() target, so an agent can drive Loopky without a screen
│
└── journeys/ # scripted end-to-end journeys + results
| 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.
./gradlew :androidApp:assembleDebugOr use the run configuration from your IDE's toolbar.
Open /iosApp in Xcode and run. shared is consumed as a static framework.
./gradlew :shared:allTests # shared KMP tests (~1,300)
./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 25 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.































