flutter_epub_viewer 2.2.1
flutter_epub_viewer: ^2.2.1 copied to clipboard
A Flutter package for viewing Epub documents developed by combining the power of Epubjs and flutter_inappwebview
Flutter Epub Viewer #
A complete EPUB reader for Flutter in one widget. It pages through books, tracks reading progress, searches, highlights, shows footnotes and applies themes, on Android, iOS, macOS and the web. Rendering is done by epub.js inside a WebView.
| Android | iOS | macOS | Web |
|---|---|---|---|
| ✅ | ✅ | ✅ (10.15+) | ✅ |
Contents: Install · Quick start · Guides · Troubleshooting · API overview · Performance · Known issues · Contributing
Install #
flutter pub add flutter_epub_viewer
Then set up the platforms you ship on:
Android: nothing for the viewer itself. If you load books from the network,
declare the INTERNET permission in android/app/src/main/AndroidManifest.xml.
Flutter only adds it to debug builds:
<uses-permission android:name="android.permission.INTERNET" />
iOS: nothing.
macOS: use a deployment target of 10.15 or higher (platform :osx, '10.15' in
macos/Podfile). Add these entitlements to both macos/Runner/DebugProfile.entitlements
and macos/Runner/Release.entitlements; the viewer serves its page and the book
from 127.0.0.1:
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
Web: nothing. See web notes for what differs.
Quick start #
import 'package:flutter/material.dart';
import 'package:flutter_epub_viewer/flutter_epub_viewer.dart';
class ReaderPage extends StatefulWidget {
const ReaderPage({super.key});
@override
State<ReaderPage> createState() => _ReaderPageState();
}
class _ReaderPageState extends State<ReaderPage> {
final controller = EpubController();
double progress = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('${(progress * 100).round()}%')),
body: SafeArea(
child: EpubViewer(
epubController: controller,
epubSource: EpubSource.fromAsset('assets/book.epub'),
onRelocated: (location) => setState(() => progress = location.progress),
),
),
);
}
}
The example app builds on this with a chapter list, search, highlights, footnote popups and error handling.
Guides #
Loading a book #
EpubSource.fromAsset('assets/book.epub')
EpubSource.fromFile(File(path))
EpubSource.fromUrl(url, headers: {'Authorization': 'Bearer …'})
EpubSource.fromData(bytes)
Sources are read only when the viewer needs them. To open another book, pass a
new source to the same EpubViewer; the previous book is released.
If a book cannot be opened (missing file, HTTP error, not an EPUB), the viewer
calls onLoadError instead of staying blank:
onLoadError: (error) => showError('Could not open the book: ${error.message}'),
Navigating #
controller.next();
controller.prev();
controller.display(cfi: chapter.href); // a chapter, or any CFI
controller.toProgressPercentage(0.5);
controller.moveToFistPage();
controller.moveToLastPage();
onChaptersLoaded gives the table of contents as EpubChapters (title,
href, nested subitems), ready for a drawer or list.
Saving and restoring progress #
Every page change calls onRelocated with an EpubLocation:
| Field | Meaning |
|---|---|
startCfi / endCfi |
Position of the page; save startCfi to resume later. |
startXpath / endXpath |
The same position as XPath, when it can be resolved. |
progress |
0–1 through the whole book. |
chapterPage / chapterTotalPages |
Screen page within the current chapter (paginated flow). |
Pass the saved CFI back as initialCfi (or an XPath as initialXPath) to resume.
progress depends on the book's locations, which the viewer computes after
opening by reading every chapter once (onLocationLoaded fires when they are
ready). Save them per book to make progress available instantly next time:
EpubViewer(
epubController: controller,
epubSource: source,
initialCfi: savedCfi,
initialLocations: savedLocations, // null on the first open
onLocationsGenerated: (locations) => store.saveLocations(bookId, locations),
onRelocated: (location) => store.saveCfi(bookId, location.startCfi),
)
Search #
final results = await controller.search(query: 'whale');
for (final r in results) {
print(r.excerpt); // text around the match
}
controller.display(cfi: results.first.cfi);
Selection, highlights and context menus #
onTextSelected gives the selected text and its CFI range. Use the range to
annotate:
controller.addHighlight(cfi: cfiRange, color: Colors.yellow);
controller.addUnderline(cfi: cfiRange);
controller.removeHighlight(cfi: cfiRange);
controller.removeUnderline(cfi: cfiRange);
Tapping an annotation calls onAnnotationClicked with its range.
Add actions to the system selection menu (Android, iOS, macOS):
selectionContextMenu: EpubContextMenu(
hideDefaultSystemItems: true,
items: [
EpubContextMenuItem(id: 1, title: 'Highlight',
action: () => controller.addHighlight(cfi: selectedCfi)),
],
),
Or draw your own menu: set suppressNativeContextMenu: true and position it
with onSelection, which gives the selection's rect. onSelectionChanging and
onDeselection tell you when to hide it.
Text helpers: extractText(startCfi:, endCfi:), extractCurrentPageText() and
getRectFromCfi(cfiRange).
Footnotes #
Set onFootnoteTapped and footnote links stop navigating away. You get an
EpubFootnote with the note's text, html and href to show in your own
popup:
onFootnoteTapped: (note) => showModalBottomSheet(
context: context,
builder: (_) => Padding(padding: const EdgeInsets.all(16), child: Text(note.text)),
),
Themes and display #
Initial settings go in displaySettings:
displaySettings: EpubDisplaySettings(
flow: EpubFlow.paginated, // or EpubFlow.scrolled
snap: true,
fontSize: 18,
theme: EpubTheme.dark(), // light(), dark() or custom(...)
),
Change them while reading with setFlow, setFontSize, setSpread,
setManager and updateTheme.
Custom themes take colors and CSS:
EpubTheme.custom(
backgroundDecoration: const BoxDecoration(color: Color(0xFFFBF0D9)),
foregroundColor: const Color(0xFF3B2F2F),
customCss: {
'p': {'font-family': 'Georgia, serif', 'line-height': '1.6'},
'::selection': {'background': 'rgba(255, 200, 0, 0.4)'},
},
);
Selection handles are drawn by the OS and can't be styled with CSS: on Android
they follow your theme's colorControlActivated / colorAccent, on iOS the
system tint.
allowScriptedContent runs scripts embedded in the book. Keep it false (the
default) for books you don't trust.
Web #
- Use
EpubSource.fromAssetorfromData.fromUrlneeds a server that sends CORS headers, andfromFileis unavailable. - The book is drawn in an
<iframe>above the Flutter scene, so Flutter widgets placed over it (aDrawer, dialog or pushed route) may not receive taps. Put controls beside the viewer, e.g. in theAppBaror a side panel, as the example app does. selectionContextMenuis ignored; useonSelectionfor a custom menu.
Troubleshooting #
macOS shows a blank page. The network.server entitlement is missing; see
Install.
fromUrl works in debug but not in release on Android. Add the INTERNET
permission; see Install.
fromUrl fails on the web. The server must allow your origin with CORS
headers. Download the bytes yourself and use fromData if you can't change it.
progress is 0 right after opening. Locations are still being generated.
Wait for onLocationLoaded, or cache them with initialLocations.
Requests like search throw TimeoutException. The viewer didn't answer in
time, e.g. a very long search. Raise controller.requestTimeout.
API overview #
Full documentation is on pub.dev.
EpubViewer
| Parameter | Description |
|---|---|
epubController, epubSource |
Required. The controller and the book. |
initialCfi, initialXPath |
Position to open at. |
initialLocations |
Cached locations from onLocationsGenerated. |
displaySettings |
Flow, snap, spread, font size, theme, direction, scripts. |
selectionContextMenu |
Extra items for the system selection menu. |
suppressNativeContextMenu |
Hide the system selection menu. Default false. |
clearSelectionOnPageChange |
Default true. |
selectAnnotationRange |
Tapping an annotation also selects its text. Default false. |
useLocalServer |
See Performance. Default true. |
| Callback | Called when |
|---|---|
onEpubLoaded |
The first page is displayed. |
onLoadError |
The book can't be opened. |
onChaptersLoaded |
The table of contents is ready. |
onRelocated |
The page changes. |
onLocationLoaded |
Locations are ready; progress is accurate from now on. |
onLocationsGenerated |
Locations were generated; cache them. |
onInitialPositionLoading / onInitialPositionLoaded |
Opening at the initial position starts / ends. |
onTextSelected |
Text is selected. |
onSelection, onSelectionChanging, onDeselection |
Selection is final / being dragged / cleared. |
onAnnotationClicked |
A highlight or underline is tapped. |
onFootnoteTapped |
A footnote link is tapped. |
onTouchDown, onTouchUp |
Touch events, coordinates normalized to 0–1. |
EpubController
| Area | Methods |
|---|---|
| Navigation | next, prev, display, toProgressPercentage, moveToFistPage, moveToLastPage |
| Information | getCurrentLocation, getChapters, getMetadata, search |
| Annotations | addHighlight, removeHighlight, addUnderline, removeUnderline |
| Text | clearSelection, extractText, extractCurrentPageText, getRectFromCfi |
| Display | setFlow, setFontSize, setSpread, setManager, updateTheme |
Performance #
On Android, iOS and macOS the viewer serves the book to the WebView by URL
(useLocalServer: true, the default). epub.js then opens it like an unpacked
folder and reads only the chapters it displays and the images, fonts and
styles they use.
- Android answers the WebView's requests itself, so no port is opened and no cleartext configuration is needed.
- iOS / macOS use a server on
127.0.0.1with a random port. Each book is reachable only under a random 128-bit token while it is open. fromUrlbooks are downloaded to a temporary file first, which is deleted when the book is closed.- ZIP64 and encrypted archives fall back to in-memory loading automatically.
With useLocalServer: false, and always on the web, the whole book is passed to
the WebView, and every image, font and stylesheet is unpacked before the first
page appears.
Measured with example/integration_test/epub_performance_test.dart: median of 3
runs, in milliseconds from mounting the viewer. These are debug builds on the
Android emulator (Pixel 8, API 36) and the iOS simulator (iPhone 17), so compare
within a row; release builds on devices are faster across the board. Search
looks for a word with thousands of hits.
| Book | Measure | 2.1.0 | 2.2.0, useLocalServer: false |
2.2.0, default |
|---|---|---|---|---|
| Illustrated, 40 chapters + 40 images (26.5 MB) | First page, Android | 7 130 | 6 372 | 1 583 |
| First page, iOS | 932 | 827 | 336 | |
| Locations ready, Android | 13 753 | 7 791 | 3 589 | |
| Locations ready, iOS | 5 030 | 1 500 | 1 018 | |
| Search, Android | 20 738 | 1 578 | 2 294 | |
| Search, iOS | 900 | 133 | 102 | |
| Text only, 300 chapters (1.2 MB) | Locations ready, Android | did not finish¹ | 11 442 | 10 117 |
| Locations ready, iOS | 31 089 | 5 551 | 5 552 | |
| Search, iOS | 8 533 | 844 | 783 | |
| Peak memory, app + WebView, Android | All three books | 1 140 MB | 822 MB | 577 MB |
¹ The test lost its connection to the app during this book, in both 2.1.0 runs.
Page turns take the same time in all three.
Known issues #
- No whole-book page count. It would require laying out every chapter. Use
progress, orchapterPage/chapterTotalPages. - Printed page numbers.
EpubLocation.printedPageis null for most books, even those with a page list (#101). - Scrolled flow. Chapter navigation can land past the requested position (#27).
- Android snap animation.
onRelocatedmay not fire whenuseSnapAnimationAndroidistrue. - OPF sources. Opening an unpacked book by its
.opffile is not fully tested.
Upgrading from 1.x #
- Use
EpubContextMenu/EpubContextMenuIteminstead offlutter_inappwebview'sContextMenu, which is no longer re-exported. - Request methods (
getCurrentLocation,search,extractText, …) return their values directly and throwTimeoutExceptioninstead of hanging.
All changes are in the CHANGELOG.
Contributing #
Issues and pull requests are welcome. Please follow the existing style and add tests with your change.
flutter test # unit tests
cd example
flutter test integration_test/epub_loading_test.dart -d <device> # end-to-end
flutter test integration_test/epub_performance_test.dart -d <device> # benchmark
tool/measure_memory.sh [android-serial] # Android peak memory
The end-to-end tests drive real WebViews in both loading modes. They cover every source type, load errors, book swapping, cached locations and lazy reading. On macOS keep the test window visible and the lid open; hidden WebViews are throttled and the tests time out.
License #
MIT. See LICENSE.
Built on epub.js and flutter_inappwebview. Thanks to everyone who has contributed: