Skip to content

Repository files navigation

a-flash-deck

Languages Downloads GitHub release (by tag) Release Android CI Release Build Emulator Test

A simple and easy to use flash card app to help you study.

Screenshots

Features

  • Easily add deck and cards
  • Rendered / editable toggle for card question and answer fields
  • Markdown rendering with LaTeX math support for card questions and answers (powered by Markwon + jlatexmath; Anki-style \( \) / \[ \] delimiters supported)
  • Reversible cards that swap question and answer during tests
  • Copy and move cards between decks
  • Save & Add Another button for quick consecutive card creation
  • Add notification timer to periodically asking you question
  • Notification time window — set start and end times for periodic reminders
  • Voice playback directly from notifications
  • Support dark mode and light mode
  • Easily export & share your decks to your friends
  • Import and export decks in Anki .apkg format (supports Basic cards with images and audio)
  • Record voices and attach images for the cards
  • Create shortcut to show random card from deck for casual study (Android 8 and above)
  • Test state persistence — resume your test after app restart
  • Flash bot to smartly suggest list of card to test you
  • AI-powered deck generation using Google Gemini API — generate from a topic, or transform existing decks (translate, expand, create harder versions)
  • AI model selection — choose from available Gemini models dynamically

Anki Integration

The app supports bidirectional import/export with Anki .apkg format:

Supported:

  • Basic notetype cards (Front/Back fields)
  • Images (JPEG, PNG) for questions and answers
  • Audio recordings (MP3) for questions and answers
  • Nested deck hierarchies (flattened to single level with " - " separator)

Limitations:

  • Only imports Basic notetype cards (other notetypes like Cloze are skipped)
  • Anki scheduling/review data is not imported or exported
  • Tags are not supported
  • HTML content is simplified (line breaks converted, basic image/audio tags preserved)

Technical Details:

  • Uses file-based SQLite database for export (Anki 2.1 format)
  • Generates valid .apkg files compatible with Anki Desktop and AnkiMobile
  • Auto-resolves deck name conflicts with numeric suffixes
  • Gracefully handles missing media files during import

Project Structure

This project is a multi-module Android application.

graph TD
    App[:app] --> Base[:base]
    App --> Bot[:bot]
    App --> Timer[:timer-notification]
    App --> AI[:ai]
    Bot --> Base
    Timer --> Base
    AI --> Base
Loading
  • :app: The main application module that contains the UI and presentation layer.
  • :base: A library module that contains base classes and utilities shared across other modules.
  • :bot: A library module that contains the logic for the "Flash bot" feature.
  • :timer-notification: A library module for handling timer-based notifications.
  • :ai: A library module for AI-powered deck generation using the Gemini API.

This project is intended for demo app for a-navigator and a-provider library usage. The app still works as production even though it is demo app.

Architecture

The app follows a modern Android architecture, utilizing a combination of established libraries and custom frameworks to create a modular and maintainable codebase.

  • Dependency Injection: The app uses a custom service locator pattern with the a-provider library. A global Provider is initialized in the MainApplication class, which is then used to provide dependencies throughout the app. This creates a centralized and easy-to-manage dependency graph.

  • Navigation: Navigation between screens is handled by the a-navigator library. This library provides a flexible and powerful way to manage navigation, including support for different screen types and transitions.

  • Reactive Programming: The app makes extensive use of RxJava for handling asynchronous operations and UI events. This allows for a more concise and readable code, especially when dealing with complex asynchronous workflows.

  • Modular Design: The app is divided into several modules, each with a specific responsibility. This promotes a clean separation of concerns and makes the codebase easier to understand and maintain.

  • Error Handling: A global exception handler is set up in MainApplication to log crashes and other unexpected errors. This helps to ensure that the app is as stable as possible.

  • Background Jobs: Background tasks are handled by WorkManager, with a custom configuration provided by MainApplication. This allows for efficient and reliable execution of background tasks, such as syncing data or sending notifications.

Presentation Layer: The StatefulView Pattern

The presentation layer is built on a custom component-based architecture centered around the StatefulView class. This pattern deviates from traditional MVP or MVVM in favor of a more self-contained and reactive approach.

Here's a breakdown of the workflow:

  • View and Logic Combined: StatefulView classes (e.g., HomePage, SettingsPage) are responsible for both creating the Android View and handling the presentation logic. This makes each StatefulView a self-contained UI component.

  • Lifecycle: The a-navigator library manages the lifecycle of StatefulViews. When you navigate to a new screen, the navigator creates the corresponding StatefulView instance. The dispose method is then called when the view is no longer needed, which is crucial for unsubscribing from RxJava streams and preventing memory leaks.

  • Dependency Injection: Dependencies are injected in two ways:

    1. The navigator injects navigation-related components (like INavigator and AppBarSV) using the @NavInject annotation.
    2. Other dependencies (like data sources, commands, and notifiers) are provided by the a-provider service locator via the provideComponent method.
  • UI Creation: The createView method is where the UI is constructed. It inflates an XML layout, finds Views by their IDs, and sets up event listeners. It also subscribes to RxJava streams to react to state changes.

  • State Management and Reactivity: The UI state is managed using RxJava's BehaviorSubject. The UI elements subscribe to these subjects, so whenever the state changes (e.g., a test starts or stops), the UI updates automatically and reactively.

  • User Interaction: User actions, handled in methods like onClick, trigger business logic by calling command classes (e.g., mNewCardCmd, mTestStateModifier.startTest). These commands perform operations and update the state, which in turn updates the UI through the reactive streams.

sequenceDiagram
    participant Nav as Navigator
    participant SV as StatefulView
    participant UI as View (XML)
    participant Rx as RxJava Subject

    Nav->>SV: Instantiate (via Provider)
    Nav->>SV: createView(activity, container)
    SV->>UI: Inflate layout
    SV->>UI: Find views & set listeners
    SV->>Rx: Subscribe to state changes
    Rx-->>SV: Emit current state
    SV->>UI: Update UI

    Note over Nav, SV: Navigation or Back Press
    Nav->>SV: dispose()
    SV->>Rx: Unsubscribe (prevent leaks)
Loading

Technical Architecture Details

Module Organization

The project follows a modular architecture with clear separation of concerns:

graph TB
    subgraph "App Module (Presentation Layer)"
        MainActivity[MainActivity]
        CardShowActivity[CardShowActivity]
        Pages[StatefulView Pages]
        Dialogs[StatefulView Dialogs]
        Components[UI Components]
        Commands[Commands]
        Receivers[Receivers]
    end

    subgraph "Base Module (Core/Foundation)"
        Entities[Room Entities]
        DAOs[DAOs]
        Database[AppDatabase]
        Notifiers[RxJava Notifiers]
        ProviderModules[Provider Modules]
        CommonUI[Common UI Components]
        Markdown[MarkdownRenderer]
        Utils[Utilities]
    end

    subgraph "Bot Module (Analytics)"
        BotEntities[Bot Entities]
        BotDAOs[Bot DAOs]
        BotDatabase[BotDatabase]
        BotAnalytics[Analytics Logic]
        BotWorkers[WorkManager Workers]
        BotCommands[Bot Commands]
    end

    subgraph "Timer Notification Module"
        TimerEntities[Timer UI Components]
        TimerPages[Timer Pages]
        TimerWorkers[Timer Workers]
        TimerCommands[Timer Commands]
    end

    subgraph "AI Module (Gemini Integration)"
        AiService[GeminiService]
        AiWorkerTopic[GenerateDeckWorker]
        AiWorkerExisting[GenerateDeckFromExistingWorker]
        AiSecurity[ApiKeyManager]
        AiUI[AI UI Components]
        AiNotifier[ApiKeyChangeNotifier]
    end

    MainActivity -->|Navigates| Pages
    MainActivity -->|Shows| Dialogs
    Pages -->|Uses| Components
    Pages -->|Executes| Commands
    Commands -->|Update| DAOs
    Commands -->|Notify| Notifiers
    DAOs -->|Access| Database

    BotWorkers -->|Access| BotDAOs
    BotDAOs -->|Access| BotDatabase
    BotWorkers -->|Uses| BotAnalytics

    TimerWorkers -->|Access| DAOs
    TimerWorkers -->|Use| TimerCommands

    AiWorker -->|Calls| AiService
    AiService -->|Access| AiSecurity
    AiWorker -->|Updates| DAOs
Loading

Layered Architecture

graph TB
    subgraph "Presentation Layer"
        A[StatefulView]
        B[XML Layouts]
        C[Navigation]
    end

    subgraph "Business Logic Layer"
        D[Commands]
        E[TestStateModifier]
        F[BotAnalytics]
        G_AI[GeminiService]
    end

    subgraph "Data Layer"
        G[DAOs]
        H[Room Database]
        I[Entities]
    end

    subgraph "Infrastructure Layer"
        J[Provider]
        K[WorkManager]
        L[RxJava Notifiers]
        M[File Helper]
    end

    A -->|Triggers| D
    A -->|Subscribes| L
    A -->|Navigates| C
    D -->|Uses| G
    G -->|Access| H
    H -->|Maps| I
    D -->|Notify| L
    K -->|Executes| D
    J -->|Provides| All
Loading

Dependency Injection (a-provider)

The app uses a custom service locator pattern through the a-provider library:

  • Provider Initialization: Created in MainApplication.onCreate() as the root Provider
  • Module Registration: Dependencies are registered via ProviderModule implementations
  • Registration Types:
    • register(): Synchronous singleton registration
    • registerAsync(): Asynchronous singleton registration (runs in background)
    • registerLazy(): Lazy singleton (created on first use)
    • registerPool(): Pool registration (creates new instances)
  • Nested Providers: Activities create nested providers with additional modules
  • Disposal: Automatic cleanup on app exit

Navigation (a-navigator)

Navigation is managed by the a-navigator library:

  • Route-Based Navigation: Uses string route constants defined in Routes class
  • Two Activities:
    • MainActivity: Main app navigation hub
    • CardShowActivity: Dedicated activity for card display shortcuts
  • Route Registration: Routes map to StatefulView factories in NavigatorProvider
  • Navigation Stack: Maintains history for back navigation
  • Transition Support: Handles screen transitions and animations

Data Persistence

The app uses Room Persistence Library with two databases:

AppDatabase (base module)

  • Entities:
    • Deck: Collection of flash cards
    • Card: Individual flash card with question/answer content, optional image and voice attachments, and a reversible-QA flag
    • Test: Test session tracking
    • AndroidNotification: Notification history
    • NotificationTimer: Scheduled notification timers

BotDatabase (bot module)

  • Entities:
    • CardLog: Tracks user interactions with cards
    • SuggestedCard: Cards suggested by the flash bot
  • Version: 1

Database Schema

Column names below are the actual Room column names. Relationships are logical only — no @ForeignKey/@Index constraints are declared. CARD.isReversed is a runtime-only @Ignore field and is not persisted.

erDiagram
    DECK ||--o{ CARD : contains
    DECK ||--o{ TEST : uses
    DECK ||--o{ NOTIFICATION_TIMER : schedules

    CARD {
        long id PK
        long deck_id
        int ordinal
        text question
        text question_image
        text question_voice
        text answer
        text answer_image
        text answer_voice
        boolean is_reversible_qa
    }

    DECK {
        long id PK
        text name
        date created_date_time
        date updated_date_time
    }

    TEST {
        long id PK
        text state_file_location
        date created_date_time
    }

    NOTIFICATION_TIMER {
        long id PK
        text name
        int period_minutes
        text selected_deck_ids
        long current_card_id
        text displayed_card_ids
    }

    CARD_LOG {
        long id PK
        long card_id
        int _action
        date created_date_time
    }

    SUGGESTED_CARD {
        long id PK
        long card_id
        date created_date_time
    }
Loading

Reactive Data Flow

The app implements a reactive unidirectional data flow:

sequenceDiagram
    participant User
    participant SV as StatefulView
    participant Cmd as Command
    participant DAO as DAO/Room
    participant Notifier
    participant Rx as RxJava
    participant DB as Database

    User->>SV: Interaction (Click)
    SV->>Cmd: Execute Command
    Cmd->>DAO: Update Data
    DAO->>DB: Execute Query
    DB-->>DAO: Result
    DAO-->>Cmd: Success
    Cmd->>Notifier: Broadcast Change
    Notifier->>Rx: Emit Event
    Rx-->>SV: OnNext(Event)
    SV->>SV: Update UI State
Loading

Command Pattern Implementation

Business logic is encapsulated in command classes following the Command pattern:

Key Commands:

  • NewDeckCmd / UpdateDeckCmd / DeleteDeckCmd: Deck management
  • NewCardCmd / UpdateCardCmd / DeleteCardCmd: Card management
  • CopyCardCmd / MoveCardCmd: Card operations
  • ExportImportCmd: Deck import/export
  • PagedDeckItemsCmd / PagedCardItemsCmd: Pagination
  • DeleteSuggestedCardCmd: Bot suggestion management
  • GenerateDeckFromTopicCmd / GenerateDeckFromExistingCmd: AI deck generation (AI module)

Command Flow:

  1. Command receives input from StatefulView
  2. Validates input
  3. Executes on background thread using ExecutorService
  4. Updates database via DAO
  5. Notifies changes via RxJava notifiers
  6. Returns Single<T> reactive type

RxJava Notifiers

Notifiers act as event hubs for data changes:

  • DeckChangeNotifier: Emits events for deck/card CRUD operations
  • TestChangeNotifier: Emits test session events (start, stop, state change)
  • NotificationTimerChangeNotifier: Emits timer configuration changes
  • NotificationTimeChangeNotifier: Emits notification time settings changes
  • SuggestedCardChangeNotifier: Emits flash bot suggestion changes
  • ApiKeyChangeNotifier: Emits API key configuration changes (AI module)

Each notifier provides Flowable<T> streams for subscription.

Background Processing

WorkManager is used for background tasks:

Key Workers:

  • NotificationTimerWorker: Executes scheduled card notifications
    • Validates time boundaries (start/end times)
    • Selects random cards from configured decks
    • Updates displayed card history
    • Posts Android notification
  • BotAnalyzeWorker: Analyzes user behavior to suggest cards
    • Tracks card interactions over 3 days
    • Calculates scores based on notification opens, test answers
    • Suggests cards with scores >= 3
  • BotLogCleanerWorker: Cleans up old card logs
  • GenerateDeckWorker: Generates flash card decks from a topic using Gemini API
    • Calls Gemini REST API with user-provided topic and card count
    • Inserts generated deck and cards into database
    • Posts success/failure notification on completion
  • GenerateDeckFromExistingWorker: Transforms existing decks (translate, expand, harder versions) via Gemini API. Both AI workers share a common BaseGenerateDeckWorker

Threading Strategy

Thread management ensures UI responsiveness:

  • Main Thread: UI updates, view inflation, event handling
  • ExecutorService: Database operations, command execution
  • ScheduledExecutorService: Scheduled tasks
  • WorkManager: Background job execution
  • RxJava Schedulers:
    • AndroidSchedulers.mainThread(): UI updates
    • ExecutorService via fromFuture(): Background operations
    • subscribeOn() / observeOn(): Thread control

Module Dependencies

app
├── implementation project(':base')
├── implementation project(':timer-notification')
├── implementation project(':bot')
├── implementation project(':ai')

timer-notification
└── implementation project(':base')

bot
└── implementation project(':base')

ai
└── implementation project(':base')

File Structure

app/src/main/java/m/co/rh/id/a_flash_deck/app/
├── MainActivity.java
├── CardShowActivity.java
├── MainApplication.java
├── anki/
│   ├── ApkgParser.java
│   ├── ApkgGenerator.java
│   └── model/ (AnkiCard, AnkiDeck, AnkiField, AnkiNote, AnkiNotetype, AnkiTemplate)
├── provider/
│   ├── AppProviderModule.java
│   ├── CommandProviderModule.java
│   ├── NavigatorProvider.java
│   ├── StatefulViewProvider.java
│   ├── StatefulViewProviderModule.java
│   ├── command/
│   ├── component/
│   └── modifier/
├── ui/
│   ├── page/ (StatefulView pages)
│   └── component/ (UI components)
└── receiver/ (Broadcast receivers)

base/src/main/java/m/co/rh/id/a_flash_deck/base/
├── entity/ (Room entities)
├── dao/ (Data access objects)
├── room/ (Database configuration)
│   └── converter/ (Type converters)
├── provider/
│   ├── BaseProviderModule.java
│   ├── DatabaseProviderModule.java
│   ├── RxProviderModule.java
│   ├── FileHelper.java
│   ├── IStatefulViewProvider.java
│   └── notifier/ (RxJava notifiers)
├── component/ (Shared components: AppSharedPreferences, AudioPlayer, AudioRecorder, MarkdownRenderer, ...)
├── constants/ (Constants, routes, keys, WorkManager keys/tags)
├── exception/ (ValidationException)
├── model/ (Event models, DeckModel, TestState)
├── repository/ (AndroidNotificationRepo)
├── rx/ (RxDisposer)
└── ui/
    ├── component/common/ (Common UI components)
    └── recyclerview/ (CustomLinearLayoutManager)

bot/src/main/java/m/co/rh/id/a_flash_deck/bot/
├── entity/ (Bot entities)
├── dao/ (Bot DAOs)
├── room/ (Bot database)
├── provider/
│   ├── BotProviderModule.java
│   ├── BotCommandProviderModule.java
│   ├── component/BotAnalytics.java
│   └── notifier/
└── workmanager/ (Bot workers)

timer-notification/src/main/java/m/co/rh/id/a_flash_deck/timer/
├── provider/
│   └── command/ (Timer commands)
├── ui/
│   ├── component/timer/ (Timer components)
│   └── page/ (Timer pages)
└── workmanager/ (Timer worker)

ai/src/main/java/m/co/rh/id/a_flash_deck/ai/
├── command/ (GenerateDeckFromTopicCmd, GenerateDeckFromExistingCmd)
├── model/ (AiGeneratedCard, AiGeneratedDeck, AvailableModel)
├── provider/
│   ├── AiProviderModule.java
│   ├── AiCommandProviderModule.java
│   └── notifier/ (ApiKeyChangeNotifier)
├── security/ (ApiKeyManager - Android Keystore encryption)
├── service/ (GeminiService - REST API via HttpURLConnection)
├── ui/
│   ├── component/settings/ (AiSettingsMenuSV)
│   └── page/ (ApiKeyEntrySVDialog, BaseGenerateDeckSVDialog, GenerateDeckFromTopicSVDialog, GenerateDeckFromExistingSVDialog)
└── workmanager/ (BaseGenerateDeckWorker, GenerateDeckWorker, GenerateDeckFromExistingWorker)

StatefulView Lifecycle

stateDiagram-v2
    [*] --> Created: Constructor
    Created --> ProvideComponent: provideComponent(Provider)
    ProvideComponent --> CreateView: createView(activity, container)
    CreateView --> Active: View is visible
    Active --> Active: User interactions
    Active --> Disposing: Navigation/Back press
    Disposing --> [*]: dispose(activity)
Loading

Key Components

Core Components:

  • Provider: Service locator for dependency injection
  • INavigator: Navigation controller
  • StatefulView: Self-contained UI component with logic
  • AppBarSV: Common app bar component
  • RxDisposer: Manages RxJava subscriptions

Data Components:

  • AppDatabase: Main Room database
  • BotDatabase: Bot analytics database
  • DAOs: Data access interfaces
  • Entities: Room database tables

Business Components:

  • Commands: Business logic operations
  • Modifiers: State modifiers (e.g., TestStateModifier)
  • Notifiers: RxJava event publishers

Testing

The project has instrumentation tests across modules:

  • app/androidTest: tests covering Anki .apkg parsing, import, export, round-trip testing, and ExportImportCmd (plus helper modules for test DB provisioning and Anki test data)
  • base/androidTest: tests covering JSON model serialization, database migrations (DbMigrationTest), and Markdown/LaTeX rendering (MarkdownRendererTest)
  • bot, timer-notification, ai: no test sources currently

Tests use isolated in-memory databases and mock dependencies for hermetic testing.

  • Areas for Improvement: The project would benefit from UI tests using a framework like Espresso to verify the application's user interface and user flows, as well as more unit tests for the business logic in the command classes.

CI/CD and Automation

The project uses a combination of GitHub Actions and Fastlane to automate the build, test, and release process.

GitHub Actions

The project has three GitHub Actions workflows:

  • gradlew-build.yml: Builds the project with Gradle on every push and pull request to the master branch.
  • android-release.yml: Creates a GitHub release and attaches the debug and release APKs when a new tag starting with "v" is pushed.
  • android-emulator-test.yml: Runs Android instrumentation tests (./gradlew connectedCheck) on an emulator matrix (API level 23 and 29, default and Google APIs targets) on every push and pull request to the master branch.

Fastlane

The project uses Fastlane to manage the app's metadata for the Google Play Store. This includes the app's title, description, screenshots, and changelogs. The metadata is stored in the fastlane/metadata/android directory and is localized for multiple languages (including en-US, id, de-DE, fr-FR, it-IT, rm, is-IS, et, nb-NO, nn-NO).

How to Build

  1. Clone the repository: git clone https://github.com/rh-id/a-flash-deck.git
  2. Open the project in Android Studio (recent Canary/preview channel recommended, since the project targets Android 17 / SDK 37 and uses AGP 9.x).
  3. Make sure JDK 21 is available — the Gradle daemon is pinned to a JDK 21 toolchain via gradle/gradle-daemon-jvm.properties.
  4. Build the project using Gradle: ./gradlew assembleDebug

The app currently targets compileSdk/targetSdk 37 (Android 17) with minSdk 23.

Libraries Used

The app uses a-navigator framework as navigator and StatefulView as base structure, combined with a-provider library for service locator, and RxJava / RxAndroid to handle UI use cases. Card questions and answers are rendered as Markdown with LaTeX math support via Markwon (including its ext-latex plugin, which brings in jlatexmath). Other notable dependencies include Room (persistence), WorkManager (background jobs), PhotoView (zoomable images), and LeakCanary's plumber-android (startup crash prevention). The Gemini API integration in the :ai module uses raw HttpURLConnection rather than a networking library.

License

This project is licensed under the GNU General Public License v3.0.

Support this project

Consider donation to support this project

https://teer.id/rh-id

Releases

Packages

Used by

Contributors

Languages