Build an ebook reader using PageView for chapter navigation, a ScrollController to track reading progress, long-press gesture to trigger text annotation dialogs, and AppState variables for font size and reading mode preferences. Store bookmarks and annotations in Firestore with the chapterId and character offset. Load chapter content on-demand from Firestore (one document per chapter) to avoid loading megabytes of text at once. This keeps the reader fast even for books with 50+ chapters.
Building an Ebook Reader That Feels Native
Ebook readers feel deceptively simple — text on a page with page turns. But a reader users actually enjoy needs several layers: smooth chapter transitions that don't jump, a progress bar that updates as they scroll, the ability to bookmark where they stopped, and font controls that respect their preference across sessions. FlutterFlow's PageView widget handles horizontal swipe chapter navigation. Custom Actions handle the scroll tracking and annotation logic that FlutterFlow's visual builder can't express. Firestore stores progress and annotations so they sync across devices. The architecture key is loading chapters one at a time rather than all at once.
Prerequisites
- A FlutterFlow project with Firebase and Firestore connected
- A Firestore 'books' collection with a 'chapters' subcollection (each chapter has title and content fields)
- FlutterFlow Standard plan or higher for Custom Actions
- Basic familiarity with FlutterFlow's widget tree and action builder
Step-by-step guide
Set Up the Firestore Schema for Books and Chapters
Set Up the Firestore Schema for Books and Chapters
Create a 'books' collection with fields: title (String), author (String), coverImageUrl (String), totalChapters (Integer), description (String). Each book document has a 'chapters' subcollection with fields: chapterIndex (Integer), title (String), content (String — the full text of the chapter), wordCount (Integer). Store content as a String field — Firestore documents support up to 1MB per document which fits most chapter lengths. If a chapter exceeds 1MB, split it into multiple documents with a part field. Create a separate 'readingProgress' collection at users/{userId}/readingProgress/{bookId} with: currentChapterIndex (Integer), scrollPosition (Double, 0.0-1.0), lastReadAt (Timestamp), completedChapters (Array of Integers).
Expected result: Your Firestore schema has books with chapters subcollection and a user-level readingProgress collection ready to store progress data.
Build the Chapter Reader with PageView
Build the Chapter Reader with PageView
Create a Reader page with a full-screen PageView widget. Set the scroll direction to horizontal and disable the physics if you want to control swiping with buttons (or leave PageScrollPhysics for natural swipe). Each page in the PageView loads one chapter's content: a Column containing a chapter title Text widget and a scrollable RichText or SelectableText widget containing the chapter content. Bind the content to a Firestore document read for the current chapter. Use a Page State variable 'currentChapterIndex' (Integer, initialized to the user's saved progress) to control the PageView's initial page. Add Previous and Next chapter navigation arrows at the bottom of the screen, updating currentChapterIndex on tap.
Expected result: Users can swipe between chapters or use navigation arrows. The PageView starts on the chapter where they last stopped reading.
Track Reading Progress with ScrollController
Track Reading Progress with ScrollController
Reading progress requires knowing how far the user has scrolled within the current chapter. Create a Custom Action called 'initScrollTracking' that attaches a ScrollController to the chapter content's SingleChildScrollView and listens for scroll position changes. On scroll, calculate progress as scrollOffset / maxScrollExtent (0.0 to 1.0). Write this value to Firestore (debounced to every 5 seconds — not on every scroll frame) at users/{userId}/readingProgress/{bookId}. Also update a local AppState variable for the progress bar at the top of the screen, which updates in real time for immediate feedback. When the page loads, restore the scroll position from the saved progress value.
1import 'dart:async';2import 'package:cloud_firestore/cloud_firestore.dart';34ScrollController? _readerScrollController;5Timer? _debounceTimer;67Future<void> initScrollTracking(8 String userId,9 String bookId,10 int chapterIndex,11 double savedScrollPosition,12) async {13 _readerScrollController?.dispose();14 _readerScrollController = ScrollController(15 initialScrollOffset: 0,16 );1718 _readerScrollController!.addListener(() {19 if (!_readerScrollController!.hasClients) return;20 final maxScroll = _readerScrollController!.position.maxScrollExtent;21 if (maxScroll <= 0) return;2223 final progress =24 _readerScrollController!.offset / maxScroll;2526 // Debounce Firestore writes to every 5 seconds27 _debounceTimer?.cancel();28 _debounceTimer = Timer(const Duration(seconds: 5), () async {29 await FirebaseFirestore.instance30 .collection('users')31 .doc(userId)32 .collection('readingProgress')33 .doc(bookId)34 .set({35 'currentChapterIndex': chapterIndex,36 'scrollPosition': progress,37 'lastReadAt': FieldValue.serverTimestamp(),38 }, SetOptions(merge: true));39 });40 });4142 // Restore saved position after layout43 WidgetsBinding.instance.addPostFrameCallback((_) {44 if (_readerScrollController!.hasClients) {45 final maxScroll =46 _readerScrollController!.position.maxScrollExtent;47 _readerScrollController!.jumpTo(savedScrollPosition * maxScroll);48 }49 });50}Expected result: A progress bar at the top updates as the user scrolls. When the user returns to the book, it opens on the exact chapter and scroll position where they left off.
Implement Text Bookmarks and Chapter Bookmarks
Implement Text Bookmarks and Chapter Bookmarks
Add a Bookmark button (bookmark icon) in the top app bar. When tapped, create a bookmark document in Firestore at users/{userId}/bookmarks/{bookmarkId} with: bookId, chapterIndex, scrollPosition, chapterTitle, and a user-provided label (optional, from a dialog). For a bookmarks list page, query this collection and display bookmark cards with the chapter title and label. Tapping a bookmark navigates back to the Reader page with the bookmarkId as a parameter, which loads the correct chapter and scroll position. Add a long-press gesture on the bookmark icon to show a 'Remove bookmark' confirmation for the current page position.
Expected result: Users can bookmark any page position, view their bookmarks list, and tap a bookmark to return to that exact position in the text.
Add Font Size and Reading Mode Controls
Add Font Size and Reading Mode Controls
Add a Settings bottom sheet accessible from the reader toolbar. The bottom sheet contains: a Slider widget for font size (range 12-24, step 1) bound to an AppState variable 'readerFontSize', a SegmentedControl for reading mode (Light / Dark / Sepia) bound to AppState 'readerTheme'. Apply the font size to the chapter content Text widget by binding its fontSize property to readerFontSize. Apply theme colors using conditional background and text colors based on readerTheme. Save both preferences to Firestore on the user document so they persist across sessions and devices. Initialize them from the user document on app startup.
Expected result: Users can adjust font size and switch between Light, Dark, and Sepia reading modes. Preferences are remembered when they return to the app.
Complete working example
1// Custom Function: getReaderBackgroundColor2Color getReaderBackgroundColor(String theme) {3 switch (theme) {4 case 'dark':5 return const Color(0xFF1A1A1A);6 case 'sepia':7 return const Color(0xFFF4ECD8);8 case 'light':9 default:10 return const Color(0xFFFFFFFF);11 }12}1314// Custom Function: getReaderTextColor15Color getReaderTextColor(String theme) {16 switch (theme) {17 case 'dark':18 return const Color(0xFFE8E8E8);19 case 'sepia':20 return const Color(0xFF5B4636);21 case 'light':22 default:23 return const Color(0xFF1A1A1A);24 }25}2627// Custom Function: estimateReadingTime28// Returns 'About 12 min' based on word count29String estimateReadingTime(int wordCount) {30 // Average reading speed: 238 words per minute31 const int wordsPerMinute = 238;32 final minutes = (wordCount / wordsPerMinute).ceil();33 if (minutes < 1) return 'Less than 1 min';34 return 'About $minutes min';35}3637// Custom Function: formatReadingProgress38// Returns '42%' or 'Complete' from a 0.0-1.0 progress double39String formatReadingProgress(double progress) {40 if (progress >= 0.98) return 'Complete';41 final percent = (progress * 100).round();42 return '$percent%';43}4445// Custom Function: getChapterProgressLabel46// Returns 'Chapter 3 of 12'47String getChapterProgressLabel(int currentChapter, int totalChapters) {48 return 'Chapter ${currentChapter + 1} of $totalChapters';49}Common mistakes when creating an Interactive Ebook Reader in FlutterFlow
Why it's a problem: Loading all chapter content at once on the reader page
How to avoid: Load chapter content on-demand: when the user navigates to a chapter, query only that chapter's Firestore document. Pre-fetch the next chapter in the background after the current one loads for instant next-chapter navigation.
Why it's a problem: Writing reading progress to Firestore on every scroll event
How to avoid: Debounce progress saves to every 5 seconds using a Timer. Cancel and restart the timer on each scroll event so the write only happens when scrolling pauses.
Why it's a problem: Using a Text widget instead of SelectableText for chapter content
How to avoid: Use SelectableText for chapter content. It renders identically to Text but enables the native text selection UI on both iOS and Android.
Best practices
- Load chapter content on-demand rather than all at once — one Firestore read per chapter navigation
- Debounce all Firestore progress saves to intervals of 5+ seconds to control write costs
- Use SelectableText for chapter content to enable native text selection
- Cache the current and next chapter in memory for instant chapter transitions
- Store reading preferences (font size, theme) on the user document for cross-device sync
- Show a reading progress bar prominently — it's a key engagement driver for reading apps
- Pre-fetch the Table of Contents document on book open rather than on first tap to reduce perceived loading time
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I'm building an ebook reader app in FlutterFlow. I need to design the Firestore data model for: books, chapters within books, user reading progress (which chapter, how far scrolled), bookmarks with chapter and position, and user annotation highlights with selected text and notes. Design the complete schema with collection names, field names, and field types.
In my FlutterFlow ebook reader, I want to track scroll position in a SingleChildScrollView and save it to Firestore every 5 seconds. The progress should be a value from 0.0 to 1.0 representing how far through the chapter the user has scrolled. Write the Custom Action dart code to attach a ScrollController, calculate progress, debounce the Firestore write, and restore scroll position from a saved value.
Frequently asked questions
How do I handle ebooks in PDF format vs plain text in FlutterFlow?
FlutterFlow doesn't have a native PDF viewer widget. For PDF ebooks, use the syncfusion_flutter_pdfviewer package as a Custom Widget — it handles PDF rendering with text selection and annotation support. For plain text or HTML content, use the approach in this tutorial (SelectableText or a flutter_html Custom Widget for formatted content).
Can I build an offline reading mode in FlutterFlow?
Firestore has built-in offline caching that automatically caches recently read documents. For intentional offline download, use a Custom Action to pre-fetch all chapter documents for a book and Firestore's enableNetwork(false) to force offline mode. Firebase Storage downloads require a Custom Action with the path_provider package to save files locally.
How do I implement a highlight/annotation feature for selected text?
This requires a Custom Widget using Flutter's RichText with TextSpan for highlighted segments. When the user selects text with SelectableText's onSelectionChanged callback, capture the start and end character offsets. Store these in Firestore as an annotation document. On render, apply different TextSpan styles to highlighted ranges.
What's the best way to implement a table of contents in FlutterFlow?
Create a separate Table of Contents page (or bottom sheet) that queries the chapters subcollection ordered by chapterIndex and displays each chapter title in a ListView. Tapping a chapter navigates to the Reader page with that chapter index as a page parameter. Store chapter titles as a separate lightweight field to avoid loading the full content in the TOC.
How do I handle very long chapters that might exceed Firestore's 1MB document limit?
Monitor your chapter word counts. A 1MB Firestore document holds roughly 200,000 characters — about 40,000 words. Most book chapters are 2,000-5,000 words and will never approach this limit. For unusually long chapters, split into 'part 1' and 'part 2' documents with a 'partIndex' field and load them sequentially.
How do I sync reading progress across multiple devices?
Firestore automatically syncs data across devices for the same authenticated user. Store progress in users/{userId}/readingProgress/{bookId} as shown in this tutorial. When the user opens the app on a different device, query this document on the Reader page initialization and restore the chapter index and scroll position.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation