Create an offline-first Android app that allows users to save, organize, and manage web links with snapshots for future reference.
Users can:
- Save links with custom title, notes, and preview image.
- Store snapshots (reader mode, PDF, or full-page screenshot) locally.
- Organize links into folders/collections.
- Optionally sync metadata (not snapshots) across devices via Appwrite.
The app must work fully offline, with optional online sync for users who want cross-device access.
| Feature | Description |
|---|---|
| Link Management | Add, edit, delete links with title, note, URL, preview image. |
| Snapshots (Local Only) | Capture reader mode text, PDF, or full-page screenshot. |
| Collections/Folders | Organize links into folders for better management. |
| Search & Filter | Search links/folders by title, note, or tags. Filter by favorites, archived, or deleted. |
| Optional Online Sync | Sync metadata (but NOT snapshots) to Appwrite. |
| Flagging/Archiving/Trash | Mark important links, archive, or move to trash. |
| Versioned Snapshots | Each link can have multiple snapshots (different timestamps). |
| Preview Auto-fetch | Auto-fetch title & preview image when a link is added. |
| Offline-First | App works completely offline with local Room DB. |
- Bottom Navigation Tabs
- Home π β All links, with search & filters.
- Collections π β User-created folders for organizing links.
- Settings βοΈ β Sync options, app settings, export/import.
- Floating Action Button (FAB) β
- Add new link (opens Add Link Page).
| Screen | Key UI Elements | Description |
|---|---|---|
| Splash | App logo + loading indicator | Check login state, network status, and perform initial sync. |
| Home | Search bar, list/grid of saved links, filters (favorites, archived). | Default landing page showing all links. |
| Collections | Folder list with counts, search bar, create/edit folder button. | Manage folders/collections. |
| Link Details | Link preview, notes, list of snapshots (PDF/Reader/Image). | View/edit link info and open snapshots. |
| Add Link | URL field, auto-fetch preview, custom title/note, folder selection, snapshot options. | Add or edit a link. |
| Snapshot Viewer | Open saved snapshot (Reader text, PDF, or full screenshot). | Full-screen view of stored content. |
| Settings | Sync controls, login/logout, export/import, theme toggle. | Manage app-wide settings and sync preferences. |
- Tech: Room DB for local storage.
- Implementation:
- Auto-fetch title & preview using a lightweight HTTP call (e.g.,
JsouporRetrofit). - Store link metadata in Room (
id,title,url,note,folderId,previewPath,previewUrl,updatedAt,syncToRemote). - Allow custom title/notes before saving.
- Auto-fetch title & preview using a lightweight HTTP call (e.g.,
- Options:
- Reader Mode β Extract main article text (via Jsoup or custom parser).
- PDF Capture β Use
PdfRendererorWebViewprinting APIs. - Full Page Screenshot β Use
WebView.capturePicture()or custom capture.
- Storage:
- Local only, saved in
filesDirorMediaStore. - Linked to
snapshottable in Room.
- Local only, saved in
- Tech: Room DB with Folder table.
- Implementation:
- Users create folders to group links.
- Each link can belong to one folder.
- CRUD operations with timestamps for sync.
- Tech: Room
LIKEqueries withFlow/LiveData. - Implementation:
- Search across
title,note, andurl. - Filters for favorites, archived, trashed.
- Search across
- Backend: Appwrite Database.
- Sync Logic:
- Only metadata (links, folders) is synced.
- Uses incremental sync (updatedAt + dirty flag).
- Snapshots remain local only.
- Add boolean flags (
isFavorite,isArchived,isDeleted) to Links table. deletedAtsupports soft delete for trash bin.
- Optional login using OTP email auth (Appwrite).
- Users without accounts can use app offline forever.
- Users with accounts can sync metadata across devices.
| Layer | Tech |
|---|---|
| Language | Kotlin |
| UI | Jetpack Compose (or XML if preferred) |
| Navigation | Jetpack Navigation Component |
| Local DB | Room with Flow |
| Backend | Appwrite (Database + Authentication) |
| Networking | Retrofit + OkHttp |
| DI | Koin |
| Background Tasks | WorkManager |
| Logging | Timber |
| Preview Fetching | Jsoup / Coil for images |
| PDF & Screenshots | Android WebView + Print APIs |
- Offline-first with incremental sync:
- Pull only remote records where
updatedAt > last_pull_time. - Push only local records with
syncToRemote = true.
- Pull only remote records where
- Snapshots never synced to Appwrite.
- Tables:
links(metadata + sync flags)folders(metadata + sync flags)snapshots(local-only)config(key-value:last_pull_time)
- Key Fields:
id,updatedAt,syncToRemote,deletedAt
- Collections:
links(metadata only, no snapshots)folders
- Key fields:
id,title,url,note,previewUrl,updatedAt,deletedAt
-
Pull Phase
- Fetch records from Appwrite where
updatedAt > last_pull_time. - Insert or update local records if remote is newer.
- Update
last_pull_timeafter successful pull.
- Fetch records from Appwrite where
-
Push Phase
- Find local records where
syncToRemote = true. - Upsert to Appwrite with latest metadata.
- Mark records as synced (
syncToRemote = false).
- Find local records where
-
Conflict Resolution
- Last-write-wins using
updatedAt. - Remote overwrites local if newer; local overwrites remote if newer.
- Last-write-wins using
- App launch (if online & logged in)
- Manual Sync Now button
- Background sync (WorkManager)
App Launch β Splash Screen
β
Check login & network
β
ββββββ Online? ββββββ
β β
No β Load local DB Yes β Incremental Sync
β
βββββββββ Pull ββββββββββ
β Fetch remote updates β
β Update local DB β
βββββββββββββββββββββββββ
β
βββββββββ Push ββββββββββ
β Upload local changes β
β Mark synced β
βββββββββββββββββββββββββ
β
App UI Ready
- π Realtime Sync via Appwrite subscriptions.
- π Link Import/Export to CSV/JSON.
- π Dark Mode toggle in settings.
- π€ Share Snapshots externally.
- π·οΈ Tagging system for links.
Enable efficient, offline-first two-way synchronization of link metadata between local Room DB and Appwrite backend, while keeping snapshots completely local.
Goals:
- Minimize network usage
- Avoid full dataset fetches/pushes
- Ensure conflict-safe updates
- Support optional login
- Track changes using timestamps (
updatedAt) and a dirty flag (syncToRemote) - Pull only records that changed since last successful pull
- Push only records marked as dirty locally
- Snapshots are never uploaded; stored locally only
Local (Room DB):
- Links Table: metadata, flags,
updatedAt,syncToRemote,deletedAt - Config Table: stores
last_pull_time
Remote (Appwrite):
- Links Collection: metadata only (
id,title,url,note,updatedAt,deletedAt,previewUrl) - Snapshots are never synced
Key Fields:
| Field | Purpose |
|---|---|
id |
Unique identifier, same locally and remotely |
updatedAt |
Last modification timestamp |
syncToRemote |
Marks local changes to push |
deletedAt |
Soft delete tracking |
last_pull_time |
Tracks last successful pull |
- Check if user is logged in (sync optional)
- Check network connectivity
- Offline β skip remote sync, load local DB
- Online β start incremental sync
- Pull Phase:
- Query Appwrite for records with
updatedAt > last_pull_time - Insert new records locally or update if remote is newer
- Query Appwrite for records with
- Push Phase:
- Find local records where
syncToRemote = true - Upsert them to Appwrite
- Mark local records as synced (
syncToRemote = false)
- Find local records where
- Manual: βSync Nowβ button
- Background: WorkManager scheduled tasks
- Optional: Realtime updates via Appwrite
- Last-write-wins using
updatedAt - Remote wins if newer, local wins if newer
- Optional: prompt user if both changed the same field simultaneously
- Add/Edit Link:
updatedAt = now,syncToRemote = true - Delete Link: set
deletedAt = now,syncToRemote = true - Snapshots updated locally, never synced
- Fast: only changed records moved
- Safe: conflict-free with last-write-wins
- Flexible: offline-first, optional login
- Scalable: easily add new tables/fields
val lastPull = configDao.get("last_pull_time") ?: 0L
val remoteChanges = appwrite.fetchLinks(updatedSince = lastPull)
for (remote in remoteChanges) {
val local = linkDao.getById(remote.id)
if (local == null) {
linkDao.insert(remote.toEntity(syncToRemote = false))
} else if (remote.updatedAt > local.updatedAt) {
linkDao.update(remote.toEntity(syncToRemote = false))
}
// else: local is newer β leave for push
}
// Update last successful pull timestamp
configDao.set("last_pull_time", System.currentTimeMillis())
val dirtyLinks = linkDao.getDirtyLinks() // where syncToRemote = true
for (link in dirtyLinks) {
appwrite.upsertLink(
id = link.id,
data = link.toRemoteMap(updatedAt = link.updatedAt)
)
linkDao.markSynced(link.id) // set syncToRemote = false
}
// When user adds, edits, or deletes a link
linkDao.update(
link.copy(
updatedAt = System.currentTimeMillis(),
syncToRemote = true
)
)
-
App launch (if online & logged in)
-
Manual βSync Nowβ
-
Background schedule (WorkManager)
-
Optional: Realtime updates trigger pull
-
Stored only locally
-
Each link can have multiple snapshots, one version per type
-
No snapshot data is ever pushed to Appwrite
User opens app β Splash Screen
β
Check login & network
β
ββββββ Online? ββββββ
β β
No β Load local DB Yes β Incremental Sync
β
ββββββββββββββββ Pull ββββββββββββββββ
β Fetch remote links updated since last_pull_time
β Insert or update local DB as needed
β Update last_pull_time
ββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββ Push βββββββββββββββββ
β Query local dirty links (syncToRemote)
β Upsert to Appwrite
β Mark synced locally
ββββββββββββββββββββββββββββββββββββββ
β
App UI fully loaded
β
Local edits β mark dirty, wait for next sync
-
Efficient delta-based sync (no full dataset transfer)
-
Conflict resolution handled automatically
-
Offline-first: app fully functional without network
-
Snapshots remain local for privacy and storage efficiency