find_in_page 2.0.2 copy "find_in_page: ^2.0.2" to clipboard
find_in_page: ^2.0.2 copied to clipboard

An in-app find bar for Flutter. Wrap your page and Ctrl+F highlights and scrolls to every match, in text you never wrapped and in lazy-list rows that were never built.

find_in_page #

A user is hunting through your release notes for the line about TextScaler, and there is no Ctrl+F to press. Mobile has no find bar at all, and on Flutter web the browser's own bar searches a canvas that contains no text. This package puts one inside the app.

import 'package:find_in_page/find_in_page.dart';

FindInPageScope(child: MyPage())

A release-notes page with the find bar open: typing narrows the highlights while the arrow buttons jump between matches and scroll each one into view

That is the whole integration. Ctrl+F (Cmd+F on macOS) opens the bar, typing highlights every match, Enter and the arrow buttons move between them and scroll each into view, and Escape closes and clears.

Text you never wrapped #

The scope searches the text Flutter actually rendered inside it, which is why widgets you did not write are covered too. Nothing in this tree implements an interface or takes a query parameter:

FindInPageScope(
  child: Column(children: [
    AppBar(title: Text('Release notes')),
    ListTile(title: Text('Release 4.0 rollout')),
    DataTable(columns: channelColumns, rows: channelRows),
    Text.rich(TextSpan(children: migrationNotice)),
  ]),
)

Four cards labelled AppBar, ListTile, DataTable and Text.rich, each a stock Flutter widget with the query highlighted inside it, under a find bar reading 1/5

That figure comes out of a test run rather than a drawing tool. tool/searchable_grid.dart builds those four widgets, types into the real find bar, and every highlight you can see was painted by the package during the capture. tool/searchable_grid.sh regenerates it.

The nearest packages on pub.dev answer a smaller question. substring_highlight and highlight_text take a string you already have and restyle the parts that match. You rewrite the widget, you supply the text, and neither one counts matches across a page or moves the viewport.

Read this before you install #

This does not fix the browser's own Ctrl+F, and nothing written in Dart can. Flutter web has two renderers left, canvaskit and skwasm, and both paint text into a canvas. Under either one the browser's find bar, its reader mode and a crawler looking for a phrase all see an empty page. Flutter has tracked that since 2020 in flutter#65504 and it is not solved here.

Older advice was to build with --web-renderer html. That renderer and that flag are both gone, and flutter build web --help no longer offers a renderer choice. When you need Chrome's own Ctrl+F to find your contact address, prerender that content as real HTML and serve it outside the canvas. An in-app find bar cannot stand in for it.

What you get here is a find bar that behaves like the browser's, on every platform Flutter runs on.

What is searchable #

Text, Text.rich, SelectableText Yes, automatically
Text inside widgets you do not own Yes, automatically
Rows of a ListView.builder that were never built Yes, via FindableListView
TextField and other editable fields No, deliberately. A browser does not match inside <input> either
Icons No. Flutter draws them as font glyphs, and those are filtered out
A collapsed ExpansionTile, an unselected tab No. It is not rendered, so there is nothing to find

Two escape hatches. ExcludeFromFind(child: ...) keeps a subtree out, which is what a navigation rail or a footer wants, and FindInPageScope(autoDiscover: false, ...) turns discovery off altogether and searches only what registered itself, the way versions before 2.0.0 behaved.

Parts #

Class Role
FindInPageScope Provides the controller, handles Ctrl+F / Escape, overlays the bar
FindableText Text replacement that registers its content and renders highlights
FindBar The search bar widget, usable standalone for custom placement
FindInPageController Query, matches, and navigation; drive it directly for custom UIs
FindableSource Interface to make any custom widget searchable
FindableListView ListView.builder adapter that makes the whole backing list searchable, including items that are not built
FindableRecord FindableSource with text supplied directly instead of read from a live widget

Screen readers #

The match counter is the whole feedback loop of a find bar: you type, and the 1/3 beside the field tells you whether that query found anything and where you are in it. Focus stays in the query field, so a screen reader never lands on that counter and the loop is silent.

FindBar announces it as a live region instead, saying "Match 1 of 3" or "No matches" whenever the count or the active match changes, while the terse 1/3 is kept out of the announcement because it reads badly aloud. The button tooltips already carried labels.

Both default to English, like the tooltips; pass a localized builder:

FindBar(
  controller: controller,
  matchStatusLabel: (active, count) =>
      count == 0 ? l10n.noMatches : l10n.matchOf(active + 1, count),
)

Custom UI #

The scope's built-in bar is optional. Drive everything yourself:

final controller = FindInPageController();

FindInPageScope(
  controller: controller,
  showBar: false,
  child: ...,
);

// Anywhere:
controller.search('flutter');   // highlights all matches
controller.next();              // moves and scrolls to the next one
print('${controller.activeMatchIndex! + 1}/${controller.matchCount}');

Highlight colors are per-widget: FindableText(highlightColor: ..., activeHighlightColor: ...).

Custom searchable widgets #

Implement FindableSource in a State and register it:

class _MyWidgetState extends State<MyWidget> implements FindableSource {
  @override
  String get findableText => widget.caption;

  @override
  BuildContext? get findableContext => mounted ? context : null;

  // register in didChangeDependencies, unregister in dispose;
  // read controller.matchesFor(this) to render your own highlights.
}

Searching a lazy list #

FindableText only registers while it is built. In a ListView.builder that means only the handful of items in the build and cache area are searchable; scrolling builds and disposes items, which registers and unregisters them and makes matchCount drift mid-session, and anything scrolled past without being built is simply missed.

Line chart of matches reported against scroll position in a 500-row list holding 43 matches. FindableListView is a flat line at 43 across all 50 positions. ListView.builder with FindableText never rises above 17, falls to 0 at 35 of the 50 positions, and the area between the two lines is shaded

Both lines come from the same 500 rows and the same query, read at 50 scroll positions. tool/lazy_list_figure.sh builds the two trees at an identical item extent, jumps each to every position and records matchCount. The shaded band is the gap between the two: matches the list holds that the count leaves out. The generator compares the arms before it writes anything, and a run where they agree produces no figure.

FindableListView closes that gap by reading each item's text straight from the backing list up front, so the whole list is searched regardless of what is built:

FindInPageScope(
  child: FindableListView(
    itemCount: items.length,
    itemExtent: 56,
    findableTextOf: (index) => items[index],
    itemBuilder: (context, index, matches, activeMatchIndex) => ListTile(
      title: Text(items[index]),
    ),
  ),
)

matches are that item's matches of the current query (empty when there are none); activeMatchIndex is which one of them is active, or null. Rendering the highlight from those offsets is the builder's job, the same way a plain ListView.builder's itemBuilder owns the whole item.

Because an off-screen match has no live widget, revealing it cannot call Scrollable.ensureVisible. FindableListView animates a ScrollController to index * itemExtent instead, which is why itemExtent is required. Every item must therefore be the same height (or width, for a horizontal list), the same constraint ListView.builder(itemExtent: ...) already carries. Variable height items are unsupported. For other data-driven cases, register a FindableRecord yourself with FindInPageController.register(record, reveal: ...), where reveal runs in place of Scrollable.ensureVisible when one of its matches becomes active.

Limits #

  • Matching is plain text and case insensitive by default; pass search(query, caseSensitive: true) for exact case. Regex is planned.
  • Match order follows widget build order, which on a normal page is top-to-bottom visual order.
  • Navigation scrolls the widget containing the active match into view. In a paragraph taller than the viewport the exact line can still be offscreen; per-line precision is planned.
  • Matches clipped away by maxLines or overflow are counted and navigated to, but they cannot become visible.
  • FindableListView needs a fixed itemExtent and renders no highlights of its own.
  • The built-in bar needs an Overlay ancestor. Every MaterialApp, CupertinoApp and WidgetsApp provides one.

License #

MIT

2
likes
0
points
947
downloads

Publisher

verified publisherdeveloperyusuf.com

Weekly Downloads

An in-app find bar for Flutter. Wrap your page and Ctrl+F highlights and scrolls to every match, in text you never wrapped and in lazy-list rows that were never built.

Repository (GitHub)
View/report issues

Topics

#search #find #highlight #text #widget

License

unknown (license)

Dependencies

flutter

More

Packages that depend on find_in_page