NativeScript开发类Anki应用:可更新离线存储方案咨询
Hey there! Building an Anki-like app with NativeScript sounds awesome—let's work through your data storage and update challenges together, step by step.
First, you'll want to use the nativescript-sqlite plugin (it's the most straightforward way to work with SQLite in NativeScript). Let's start with a solid table structure that supports your review needs:
You'll need two core tables at minimum:
cards: Stores all flashcard content and review metadataapp_metadata: Tracks data versions to handle incremental updates
Here's a sample SQL script to create these tables (you can run this on app first launch):
import { openDatabase } from "nativescript-sqlite"; async function initDatabase() { const db = await openDatabase("flashcards.db"); // Create cards table await db.execSQL(` CREATE TABLE IF NOT EXISTS cards ( card_id TEXT PRIMARY KEY, -- Use a unique, stable ID (like a UUID) for each default card front TEXT NOT NULL, back TEXT NOT NULL, next_review INTEGER NOT NULL, -- Unix timestamp for next scheduled review review_count INTEGER DEFAULT 0, is_user_modified INTEGER DEFAULT 0 -- 0 = default card, 1 = user edited this card ) `); // Create metadata table for version tracking await db.execSQL(` CREATE TABLE IF NOT EXISTS app_metadata ( key TEXT PRIMARY KEY, value TEXT NOT NULL ) `); // Initialize data version if it doesn't exist const versionCheck = await db.get("SELECT value FROM app_metadata WHERE key = 'data_version'"); if (!versionCheck) { await db.execSQL("INSERT INTO app_metadata (key, value) VALUES ('data_version', '0')"); } }
For the spaced repetition logic, you can calculate the next_review timestamp based on how well the user remembers the card. A simple starting point (you can refine this later like Anki's SM-2 algorithm):
- If user marks "Easy": Set next review to current time + 1 day
- If user marks "Good": Set next review to current time + 10 minutes
- If user marks "Hard": Set next review to current time + 5 minutes
When querying cards to review, just filter for next_review <= CURRENT_TIMESTAMP:
async function getDueCards() { const db = await openDatabase("flashcards.db"); const now = Date.now(); return db.all("SELECT * FROM cards WHERE next_review <= ?", [now]); }
This is the key part—you want to add new cards on app updates without overwriting user data. Here's a rock-solid approach using versioned built-in cards:
How it works:
- Bundle versioned card data with your app: Store new cards in JSON files in your app's
assetsfolder (e.g.,cards_v1.json,cards_v2.json). Each file should have aversionfield and an array of cards with uniquecard_ids. - Track the latest imported version: Use the
app_metadatatable to store the highest version of cards the user has already imported. - On app launch, check for new versions: Compare the local stored version with the latest version bundled in the app. If there's a newer version, import only those new cards—skip any cards the user has modified.
Sample implementation:
First, create a cards_v2.json in assets/data/:
{ "version": "2", "cards": [ { "card_id": "math_algebra_001", "front": "What's the quadratic formula?", "back": "x = [-b ± √(b²-4ac)]/(2a)", "next_review": 0, // Will be set to current time + default interval on first import "review_count": 0 } ] }
Then, add an update check function that runs on app start:
import { knownFolders } from "@nativescript/core"; async function checkForCardUpdates() { const db = await openDatabase("flashcards.db"); const localVersion = await db.get("SELECT value FROM app_metadata WHERE key = 'data_version'"); const latestVersion = "2"; // Hardcode this for the current app version, or read from a config file if (parseInt(localVersion.value) < parseInt(latestVersion)) { // Load the new cards from assets const cardsFile = knownFolders.currentApp().getFile("data/cards_v2.json"); const newCardsData = await cardsFile.readText(); const newCards = JSON.parse(newCardsData).cards; // Use a transaction to batch insert (faster and atomic) await db.execSQL("BEGIN TRANSACTION"); for (const card of newCards) { // Check if the card already exists, and if the user hasn't modified it const existingCard = await db.get("SELECT is_user_modified FROM cards WHERE card_id = ?", [card.card_id]); if (!existingCard) { // Insert new card, set initial next_review to now (so it shows up immediately) await db.execSQL(` INSERT INTO cards (card_id, front, back, next_review) VALUES (?, ?, ?, ?) `, [card.card_id, card.front, card.back, Date.now()]); } else if (existingCard.is_user_modified === 0) { // Optional: Update default cards if you need to fix errors (skip if you don't want to overwrite) await db.execSQL(` UPDATE cards SET front = ?, back = ? WHERE card_id = ? `, [card.front, card.back, card.card_id]); } } await db.execSQL("COMMIT"); // Update the local version to the latest await db.execSQL("UPDATE app_metadata SET value = ? WHERE key = 'data_version'", [latestVersion]); } }
Bonus: Protect User-Created Cards
If you want users to create their own cards, add a card_type field (e.g., default vs user_created) to the cards table. When updating, never touch user_created cards—only handle the default ones.
- Run DB operations off the main thread: Use
async/awaitproperly, and consider wrapping heavy operations in a background task to avoid UI freezes. - Add backup functionality: Let users export their database to the device's storage (or cloud, if you want) so they don't lose data if they reinstall the app.
- Test edge cases: What if the user skips multiple app updates? Make sure your version check logic can jump from v1 to v3 without missing v2's cards.
内容的提问来源于stack exchange,提问作者Hoargarth

