lazy_text_field
Pure Flutter lazy text-field primitives for table and grid cells.
LazyTextField renders read-only text without allocating edit resources, then
connects a real TextField only while editing. It also exposes fast
constrained-width height calculation through LazyTextFieldLayout,
LazyTextFieldMetrics, and LazyTextField.computeHeightForWidth.
Live demo
Explore the full table demo in the browser:
The live demo covers edit-only and always-visible decoration, constrained wrapping, bounded height with scrollbar gutter, prefix/suffix icons, overflow markers, and lazy controller allocation in a multi-column table.
Even on the full page, only one real TextField / EditableText is mounted at
a time. Every other cell stays on the lightweight read-only path until it is
activated.
Run the same demo locally with:
cd example
flutter run -d chrome -t lib/main_live_demo.dart
Installation
Add lazy_text_field to your pubspec.yaml:
dependencies:
lazy_text_field: ^1.0.3
Then import the public barrel:
import 'package:lazy_text_field/lazy_text_field.dart';
Why not just TextField?
TextField in every cell |
LazyTextField |
|
|---|---|---|
| Controllers / focus nodes | One per visible cell | Created only for the active editor |
| Read-only rendering | Still pays InputDecorator layout cost |
Lightweight Text / painter path |
| Static to edit switch | Often shifts line wraps or height | Keeps the same text layout boxes |
| Row height in tables | Hard to match edit height exactly | computeHeightForWidth uses the same engine as the widget |
| Decoration in grids | InputDecoration affects intrinsic height |
LazyInputDecoration is height-neutral chrome |
Use LazyTextField when you render hundreds or thousands of editable cells and
need predictable memory use, stable row heights, and pixel-aligned transitions
between read-only and edit modes.
The real cost is the mounted TextField
For big tables and grids, the expensive part is not just text painting. The
cost appears when every visible cell mounts a real TextField.
A Flutter TextField builds an EditableText. EditableTextState participates
in a lot of editing infrastructure, including:
TextInputClientAutofillClientWidgetsBindingObserverTickerProviderStateMixinAutomaticKeepAliveClientMixin- text-selection behavior
- clipboard monitoring
- spell-check behavior
- cursor animation
- scrolling
- keyboard input connection
- selection overlays and context menus
During initialization, EditableTextState registers controller and focus
listeners, clipboard listeners, lifecycle handling, spell-check configuration,
and other editing infrastructure. EditableText also handles keyboard input
through the platform text-input service, selection and cursor movement, keeping
the caret visible, scrolling, input formatters, selection menus, and gestures.
That is all correct for the active editor. It is unnecessary for thousands of
read-only cells. LazyTextField keeps the table cheap by rendering static cells
with a lightweight text path, then mounting the real TextField only for the
cell currently being edited.
Choose a widget
| Widget | When to use |
|---|---|
LazyTextField |
You already own controller/focus state and pass them only while editing |
StatefulLazyTextField |
One-off cell where the widget can own its edit lifecycle |
LazyTextFieldControllerScope + ScopedLazyTextField |
Many cells sharing one edit manager |
Usage
LazyTextField(
cellId: 'row-42-title',
text: value,
isEditing: controller != null && focusNode != null,
controller: controller,
focusNode: focusNode,
style: const TextStyle(fontSize: 14, height: 1.25),
padding: kDefaultLazyTextFieldPadding,
decoration: LazyInputDecoration(
filled: true,
border: const OutlineInputBorder(),
hintText: 'Click to edit',
prefixIcon: const Icon(Icons.notes, size: 16),
prefixIconConstraints: const BoxConstraints(minWidth: 28),
),
decorationVisibility: LazyInputDecorationVisibility.editing,
onStartEditing: startEditing,
onChanged: updateDraft,
)
When isEditing is true, provide both controller and focusNode. When it is
false, pass null for both so read-only mode stays lazy.
For simple local state, use StatefulLazyTextField. For many cells sharing one
edit manager, wrap a subtree in LazyTextFieldControllerScope and use
ScopedLazyTextField.
StatefulLazyTextField and ScopedLazyTextField can control the initial
selection with startEditSelection: beginning, end, tapPosition, or
fullSelection.
Decoration
LazyInputDecoration is height-neutral chrome drawn outside the internal
TextField, which always receives decoration: null. Supported fields are
fill, hover/focus/error/disabled borders, hint text/style, prefix icon, suffix
icon, and icon constraints.
Decoration does not add padding. LazyTextField.padding is the only content
padding source.
Overflow marker
When read-only text is clipped, LazyTextField shows a default red corner
marker. Customize it with overflowMarkerBuilder, or pass null to hide it:
LazyTextField(
cellId: 'row-42-notes',
text: value,
isEditing: false,
onStartEditing: startEditing,
overflowMarkerBuilder: (context, details) {
return Icon(
details.expanded ? Icons.unfold_less : Icons.more_horiz,
size: details.size,
color: details.color,
);
},
)
The package still owns marker positioning and tap handling, so marker taps call
onReadOnlyOverflowToggle without also starting cell editing. Use
LazyTextField.defaultOverflowMarkerBuilder to delegate back to the default
triangle from a custom builder.
Height Measurement
final height = LazyTextField.computeHeightForWidth(
text: value,
width: columnWidth,
style: const TextStyle(fontSize: 14, height: 1.25),
padding: kDefaultLazyTextFieldPadding,
reservedLeadingWidth: 28,
reservedTrailingWidth: 28,
);
The layout path uses TextPainter, the effective text width, strut style, text
direction, text scaler, a zero-width space for empty text and trailing newlines,
and ceiled final heights.
For bounded editing, scrollbarGutter reserves text width for the package
scrollbar. Use scrollbarThickness and scrollbarAlignment to control the
thumb size and where it sits inside that reserved gutter.
License
MIT - see LICENSE.
Libraries
- lazy_text_field
- Pure-Flutter lazy text fields for table and grid cells.