perf(inventory): lazy list, photo thumbnails, indexes, debounced reload
Scale the local inventory to 10k+ varieties: - Render the list with ListView.builder over a flattened header/item model instead of building every tile upfront. - Store a small regenerable JPEG thumbnail per photo (schema v14) and use it for the 48px list avatar; full bytes stay for offer image hosting. Existing photos are backfilled lazily at startup. Thumbnail is local-only (excluded from CRDT sync and backups by the JSON codec). - Add indexes on varieties(is_deleted,is_draft), attachments(parent_type, parent_id,kind), lots(variety_id) via @TableIndex. - Debounce watchInventoryView (~250ms) so a burst of table writes triggers one reload, not seven. - cacheWidth/cacheHeight on the list avatar decode. - Scale test raised 3k -> 10k; migration test v13 -> v14.
This commit is contained in:
parent
2884ddd3c7
commit
3de01bd948
12 changed files with 5114 additions and 34 deletions
|
|
@ -582,27 +582,47 @@ class _InventoryBody extends StatelessWidget {
|
|||
}
|
||||
|
||||
// Items arrive ordered by category then label; insert a header whenever the
|
||||
// category changes.
|
||||
final rows = <Widget>[];
|
||||
// category changes. Precompute a flat row model (header or item) so the
|
||||
// ListView can build tiles lazily — with a large inventory, building every
|
||||
// tile upfront (the old `ListView(children:)`) stalled the first paint.
|
||||
final rows = <_InventoryRow>[];
|
||||
String? currentCategory;
|
||||
for (final item in items) {
|
||||
final category = item.category ?? t.inventory.uncategorized;
|
||||
if (category != currentCategory) {
|
||||
currentCategory = category;
|
||||
rows.add(_CategoryHeader(title: category));
|
||||
rows.add(_InventoryRow.header(category));
|
||||
}
|
||||
rows.add(
|
||||
_VarietyTile(
|
||||
item: item,
|
||||
selectionMode: selectionMode,
|
||||
selected: selectedIds.contains(item.id),
|
||||
),
|
||||
);
|
||||
rows.add(_InventoryRow.variety(item));
|
||||
}
|
||||
return ListView(children: rows);
|
||||
return ListView.builder(
|
||||
itemCount: rows.length,
|
||||
itemBuilder: (context, i) {
|
||||
final row = rows[i];
|
||||
final header = row.header;
|
||||
if (header != null) {
|
||||
return _CategoryHeader(title: header);
|
||||
}
|
||||
return _VarietyTile(
|
||||
item: row.item!,
|
||||
selectionMode: selectionMode,
|
||||
selected: selectedIds.contains(row.item!.id),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A single row in the flattened inventory list: either a category [header] or
|
||||
/// a variety [item] (exactly one is non-null).
|
||||
class _InventoryRow {
|
||||
const _InventoryRow.header(this.header) : item = null;
|
||||
const _InventoryRow.variety(this.item) : header = null;
|
||||
|
||||
final String? header;
|
||||
final VarietyListItem? item;
|
||||
}
|
||||
|
||||
class _CategoryHeader extends StatelessWidget {
|
||||
const _CategoryHeader({required this.title});
|
||||
|
||||
|
|
@ -767,10 +787,21 @@ class _Avatar extends StatelessWidget {
|
|||
// Decorative: the tile title already announces the variety name, so keep
|
||||
// the thumbnail / initial out of the semantics tree.
|
||||
if (photo != null) {
|
||||
// Decode straight to the 48px avatar size (× device pixel ratio) instead
|
||||
// of holding the full-resolution photo in the image cache — decisive for
|
||||
// memory with a large inventory of photographed varieties.
|
||||
final cachePx = (48 * MediaQuery.devicePixelRatioOf(context)).round();
|
||||
return ExcludeSemantics(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.memory(photo, width: 48, height: 48, fit: BoxFit.cover),
|
||||
child: Image.memory(
|
||||
photo,
|
||||
width: 48,
|
||||
height: 48,
|
||||
cacheWidth: cachePx,
|
||||
cacheHeight: cachePx,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue