define a type for id and use uuids instead of existing int

This commit is contained in:
2025-07-27 20:03:13 +00:00
parent 61d0ec4e67
commit 0b4ffc1ade
8 changed files with 117 additions and 66 deletions
+9 -4
View File
@@ -12,6 +12,11 @@ class DynamicWidgetExample extends StatefulWidget {
class _DynamicWidgetExampleState extends State<DynamicWidgetExample> { class _DynamicWidgetExampleState extends State<DynamicWidgetExample> {
final LazyCanvasController controller = LazyCanvasController(debug: false); final LazyCanvasController controller = LazyCanvasController(debug: false);
// Store child IDs for later reference
late CanvasChildId selfManagedId;
late CanvasChildId externalDataId;
late CanvasChildId reactiveId;
// Simulate some dynamic data // Simulate some dynamic data
int counter = 0; int counter = 0;
String message = "Hello World"; String message = "Hello World";
@@ -25,16 +30,16 @@ class _DynamicWidgetExampleState extends State<DynamicWidgetExample> {
void _setupWidgets() { void _setupWidgets() {
// Approach 1: Using StatefulWidget with state inside the widget getting lost if unmounted // Approach 1: Using StatefulWidget with state inside the widget getting lost if unmounted
controller.addChild(const Offset(0, 0), SelfManagedCounterWidget(label: "Self-Managed")); selfManagedId = controller.addChild(const Offset(0, 0), SelfManagedCounterWidget(label: "Self-Managed"));
// Approach 2: Using updateChildWidget, you need this since the children dep tree is direcly managed by the controller // Approach 2: Using updateChildWidget, you need this since the children dep tree is direcly managed by the controller
controller.addChild( externalDataId = controller.addChild(
const Offset(200, 0), const Offset(200, 0),
ExternalDataWidget(counter: counter, message: message, color: selectedColor), ExternalDataWidget(counter: counter, message: message, color: selectedColor),
); );
// Approach 3: Using ValueListenableBuilder for reactive updates // Approach 3: Using ValueListenableBuilder for reactive updates
controller.addChild( reactiveId = controller.addChild(
const Offset(400, 0), const Offset(400, 0),
ValueListenableBuilder<int>( ValueListenableBuilder<int>(
valueListenable: _counterNotifier, valueListenable: _counterNotifier,
@@ -71,7 +76,7 @@ class _DynamicWidgetExampleState extends State<DynamicWidgetExample> {
// Approach 2: Update the widget when external data changes // Approach 2: Update the widget when external data changes
// Use ValueKey to ensure Flutter recognizes this as a different widget // Use ValueKey to ensure Flutter recognizes this as a different widget
controller.updateChildWidget( controller.updateChildWidget(
1, // Widget ID (second widget added) externalDataId, // Use the stored widget ID
ExternalDataWidget( ExternalDataWidget(
// key: ValueKey('external_$counter'), // Force rebuild with unique key // key: ValueKey('external_$counter'), // Force rebuild with unique key
counter: counter, counter: counter,
+4 -2
View File
@@ -12,21 +12,23 @@ class SimpleExample extends StatefulWidget {
class _SimpleExampleState extends State<SimpleExample> { class _SimpleExampleState extends State<SimpleExample> {
final LazyCanvasController controller = LazyCanvasController(debug: true); final LazyCanvasController controller = LazyCanvasController(debug: true);
final List<CanvasChildId> childIds = [];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
for (int i = 0; i < 10; i++) { for (int i = 0; i < 10; i++) {
controller.addChild( final id = controller.addChild(
Offset((i % 80) * 100.0, (i ~/ 80) * 100.0), Offset((i % 80) * 100.0, (i ~/ 80) * 100.0),
GestureDetector( GestureDetector(
onTap: () { onTap: () {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Tapped a container'))); ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Tapped a container')));
controller.focusOnChild(i); controller.focusOnChild(childIds[i]);
}, },
child: InfoContainer(color: Colors.primaries[i % Colors.primaries.length]), child: InfoContainer(color: Colors.primaries[i % Colors.primaries.length]),
), ),
); );
childIds.add(id);
} }
controller.addChild( controller.addChild(
const Offset(0, 200), const Offset(0, 200),
+40
View File
@@ -41,6 +41,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.19.1" version: "1.19.1"
crypto:
dependency: transitive
description:
name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://pub.dev"
source: hosted
version: "3.0.6"
cupertino_icons: cupertino_icons:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -57,6 +65,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.3" version: "1.3.3"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@@ -159,6 +175,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.10.1" version: "1.10.1"
sprintf:
dependency: transitive
description:
name: sprintf
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@@ -199,6 +223,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.4" version: "0.7.4"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
uuid:
dependency: transitive
description:
name: uuid
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
url: "https://pub.dev"
source: hosted
version: "4.5.1"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
+19 -18
View File
@@ -9,26 +9,27 @@ import '../../utils/size_extensions.dart';
import '../../utils/conversions.dart'; import '../../utils/conversions.dart';
import '../../utils/styles.dart'; import '../../utils/styles.dart';
import '../render.dart'; import '../render.dart';
import 'package:uuid/uuid.dart';
part 'debug.dart'; part 'debug.dart';
part 'types.dart'; part 'types.dart';
/// Controller for [LazyCanvas] /// Controller for [LazyCanvas]
class LazyCanvasController with ChangeNotifier { class LazyCanvasController with ChangeNotifier {
int _nextId = 0; // surely we won't run out of IDs, Clueless final Uuid _uuid = Uuid();
Offset _gsTopLeftOffset = Offset.zero; Offset _gsTopLeftOffset = Offset.zero;
double _baseScale = 1, _scale = 1; double _baseScale = 1, _scale = 1;
late Size _canvasSize; late Size _canvasSize;
final HashMap<int, _ChildInfo> _children = HashMap<int, _ChildInfo>(); // int for IDs final HashMap<CanvasChildId, _ChildInfo> _children = HashMap<CanvasChildId, _ChildInfo>(); // CanvasChildId for IDs
final Offset? _buildCacheExtent; final Offset? _buildCacheExtent;
late Offset _buildExtent; late Offset _buildExtent;
final Size _hashCellSize; final Size _hashCellSize;
bool _init = false; bool _init = false;
late final SpatialHashing<int> _spatialHash; late final SpatialHashing<CanvasChildId> _spatialHash;
TickerProvider? _ticker; TickerProvider? _ticker;
late BuildContext _context; late BuildContext _context;
bool _firstBuild = true; bool _firstBuild = true;
int? _focusChildOnInit; // if set, will focus on this child on first build CanvasChildId? _focusChildOnInit; // if set, will focus on this child on first build
CanvasBackground background; CanvasBackground background;
// these are used to cache result of widgetsWithScreenPositions // these are used to cache result of widgetsWithScreenPositions
List<ChildInfo> _lastRenderedWidgets = []; List<ChildInfo> _lastRenderedWidgets = [];
@@ -50,7 +51,7 @@ class LazyCanvasController with ChangeNotifier {
// only top left is considered so if a widget has long width, it'll not be rendered // only top left is considered so if a widget has long width, it'll not be rendered
// unless the cache extent is sufficient // unless the cache extent is sufficient
{ {
_spatialHash = SpatialHashing<int>(cellSize: _hashCellSize); _spatialHash = SpatialHashing<CanvasChildId>(cellSize: _hashCellSize);
} }
// ==================== Getters ==================== // ==================== Getters ====================
@@ -79,7 +80,7 @@ class LazyCanvasController with ChangeNotifier {
} }
/// Called when a child widget's size changes. /// Called when a child widget's size changes.
void onChildSizeChange(int id, Size size) { void onChildSizeChange(CanvasChildId id, Size size) {
_children[id]!.lastRenderedSize = size; _children[id]!.lastRenderedSize = size;
} }
@@ -107,23 +108,24 @@ class LazyCanvasController with ChangeNotifier {
/// Add a child at a given position with a widget. Returns the child ID. /// Add a child at a given position with a widget. Returns the child ID.
/// You need the child size for optimising the focus on child /// You need the child size for optimising the focus on child
int addChild(Offset position, Widget widget, {bool focusOnInit = false, Size? childSize}) { CanvasChildId addChild(Offset position, Widget widget, {bool focusOnInit = false, Size? childSize}) {
final id = _uuid.v4();
if (focusOnInit) { if (focusOnInit) {
assert( assert(
childSize != null, childSize != null,
'Child size must be provided when focusing on init (to know positions before any build). Else, you must call focusOnChild in a post frame callback.', 'Child size must be provided when focusing on init (to know positions before any build). Else, you must call focusOnChild in a post frame callback.',
); );
assert(_focusChildOnInit == null, 'Focus child on init is already set. Cannot set it again.'); assert(_focusChildOnInit == null, 'Focus child on init is already set. Cannot set it again.');
_focusChildOnInit = _nextId; _focusChildOnInit = id;
} }
_children[_nextId] = _ChildInfo(gsPosition: position, widget: widget, lastRenderedSize: childSize); _children[id] = _ChildInfo(gsPosition: position, widget: widget, lastRenderedSize: childSize);
_spatialHash.add(position.toPoint(), _nextId); // add to spatial hash _spatialHash.add(position.toPoint(), id); // add to spatial hash
markDirty(); markDirty();
return _nextId++; return id;
} }
/// Remove a child by its ID. /// Remove a child by its ID.
void removeChild(int id) { void removeChild(CanvasChildId id) {
if (!_children.containsKey(id)) { if (!_children.containsKey(id)) {
throw _ChildNotFoundException; throw _ChildNotFoundException;
} }
@@ -136,12 +138,11 @@ class LazyCanvasController with ChangeNotifier {
void clear() { void clear() {
_children.clear(); _children.clear();
_spatialHash.clear(); _spatialHash.clear();
_nextId = 0;
markDirty(); markDirty();
} }
/// Update the position of a child by its ID. /// Update the position of a child by its ID.
int updatePosition(int id, Offset newPosition) { CanvasChildId updatePosition(CanvasChildId id, Offset newPosition) {
if (_children.containsKey(id)) { if (_children.containsKey(id)) {
_children[id]!.gsPosition = newPosition; _children[id]!.gsPosition = newPosition;
markDirty(); markDirty();
@@ -152,7 +153,7 @@ class LazyCanvasController with ChangeNotifier {
} }
/// Update a child's widget. /// Update a child's widget.
void updateChildWidget(int id, Widget newWidget) { void updateChildWidget(CanvasChildId id, Widget newWidget) {
if (_children.containsKey(id)) { if (_children.containsKey(id)) {
final child = _children[id]!; final child = _children[id]!;
// Create a new _ChildInfo with the new widget // Create a new _ChildInfo with the new widget
@@ -165,7 +166,7 @@ class LazyCanvasController with ChangeNotifier {
} }
/// Get the position of a child by its ID. /// Get the position of a child by its ID.
Offset getPosition(int id) { Offset getPosition(CanvasChildId id) {
if (_children.containsKey(id)) { if (_children.containsKey(id)) {
return _children[id]!.gsPosition; return _children[id]!.gsPosition;
} else { } else {
@@ -244,7 +245,7 @@ class LazyCanvasController with ChangeNotifier {
}).toList(); }).toList();
} }
List<int> _childrenWithinBuildArea(Offset center, Offset extent) { List<CanvasChildId> _childrenWithinBuildArea(Offset center, Offset extent) {
Offset halfExtent = Offset((extent.dx / (2 * _scale)).ceilToDouble(), (extent.dy / (2 * _scale)).ceilToDouble()); Offset halfExtent = Offset((extent.dx / (2 * _scale)).ceilToDouble(), (extent.dy / (2 * _scale)).ceilToDouble());
final items = _spatialHash.getPointsAround(center.toPoint(), halfExtent); final items = _spatialHash.getPointsAround(center.toPoint(), halfExtent);
return items.map((item) => item.data).toList(); // data is the child id here return items.map((item) => item.data).toList(); // data is the child id here
@@ -274,7 +275,7 @@ class LazyCanvasController with ChangeNotifier {
/// an offstage rendering will be used ( double render ) /// an offstage rendering will be used ( double render )
/// Preferred horizontal margin used for [ScalingMode.fitInViewport]. /// Preferred horizontal margin used for [ScalingMode.fitInViewport].
void focusOnChild( void focusOnChild(
int id, { CanvasChildId id, {
ScalingMode scalingMode = ScalingMode.keepScale, ScalingMode scalingMode = ScalingMode.keepScale,
bool animate = true, bool animate = true,
double preferredHorizontalMargin = 16, double preferredHorizontalMargin = 16,
+1 -1
View File
@@ -1,7 +1,7 @@
part of 'controller.dart'; part of 'controller.dart';
class _Debug extends StatelessWidget { class _Debug extends StatelessWidget {
final int id; final CanvasChildId id;
final Offset gs, ss; final Offset gs, ss;
final Widget child; final Widget child;
+3 -1
View File
@@ -15,7 +15,7 @@ class _ChildInfo {
} }
class ChildInfo { class ChildInfo {
int id; CanvasChildId id;
Offset gsPosition; Offset gsPosition;
Offset ssPosition; Offset ssPosition;
Widget child; Widget child;
@@ -23,3 +23,5 @@ class ChildInfo {
} }
enum ScalingMode { resetScale, keepScale, fitInViewport } enum ScalingMode { resetScale, keepScale, fitInViewport }
typedef CanvasChildId = String;
+4 -4
View File
@@ -81,7 +81,7 @@ class _LazyCanvasState extends State<LazyCanvas> with TickerProviderStateMixin<L
/// A combined widget for all the render object of the children + background. /// A combined widget for all the render object of the children + background.
/// Everything is in screen space here /// Everything is in screen space here
class _CanvasRenderObject extends MultiChildRenderObjectWidget { class _CanvasRenderObject extends MultiChildRenderObjectWidget {
final List<int> childrenIds; final List<CanvasChildId> childrenIds;
final List<Offset> ssPositions; final List<Offset> ssPositions;
final double scale; final double scale;
final Offset gridSpaceOffset; final Offset gridSpaceOffset;
@@ -127,7 +127,7 @@ class _CanvasRenderObject extends MultiChildRenderObjectWidget {
class _CanvasWidgetParentData extends ContainerBoxParentData<RenderBox> { class _CanvasWidgetParentData extends ContainerBoxParentData<RenderBox> {
// there is already an "offset" defined in BoxParentdata that is exactly what I want // there is already an "offset" defined in BoxParentdata that is exactly what I want
late int id; late CanvasChildId id;
late double scale; // scale is same for all rn but this makes it trivial to expand to children with diff scales late double scale; // scale is same for all rn but this makes it trivial to expand to children with diff scales
} }
@@ -136,7 +136,7 @@ class _CanvasRenderBox extends RenderBox
ContainerRenderObjectMixin<RenderBox, _CanvasWidgetParentData>, ContainerRenderObjectMixin<RenderBox, _CanvasWidgetParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, _CanvasWidgetParentData> { RenderBoxContainerDefaultsMixin<RenderBox, _CanvasWidgetParentData> {
CanvasBackground _canvasBackground; CanvasBackground _canvasBackground;
List<int> _childrenIds; List<CanvasChildId> _childrenIds;
List<Offset> _ssPositions; List<Offset> _ssPositions;
Offset _gridSpaceOffset; Offset _gridSpaceOffset;
double _scale; double _scale;
@@ -175,7 +175,7 @@ class _CanvasRenderBox extends RenderBox
} }
} }
set childrenIds(List<int> childrenIds) { set childrenIds(List<CanvasChildId> childrenIds) {
if (_childrenIds != childrenIds) { if (_childrenIds != childrenIds) {
_childrenIds = childrenIds; _childrenIds = childrenIds;
} }
+37 -36
View File
@@ -4,17 +4,18 @@ version: 0.0.1
homepage: homepage:
environment: environment:
sdk: ^3.8.1 sdk: ^3.8.1
flutter: ">=1.17.0" flutter: ">=1.17.0"
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
uuid: ^4.5.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
flutter_lints: ^5.0.0 flutter_lints: ^5.0.0
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec
@@ -22,33 +23,33 @@ dev_dependencies:
# The following section is specific to Flutter packages. # The following section is specific to Flutter packages.
flutter: flutter:
# To add assets to your package, add an assets section, like this: # To add assets to your package, add an assets section, like this:
# assets: # assets:
# - images/a_dot_burr.jpeg # - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg # - images/a_dot_ham.jpeg
# #
# For details regarding assets in packages, see # For details regarding assets in packages, see
# https://flutter.dev/to/asset-from-package # https://flutter.dev/to/asset-from-package
# #
# An image asset can refer to one or more resolution-specific "variants", see # An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images # https://flutter.dev/to/resolution-aware-images
# To add custom fonts to your package, add a fonts section here, # To add custom fonts to your package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a # in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a # "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For # list giving the asset and other descriptors for the font. For
# example: # example:
# fonts: # fonts:
# - family: Schyler # - family: Schyler
# fonts: # fonts:
# - asset: fonts/Schyler-Regular.ttf # - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf # - asset: fonts/Schyler-Italic.ttf
# style: italic # style: italic
# - family: Trajan Pro # - family: Trajan Pro
# fonts: # fonts:
# - asset: fonts/TrajanPro.ttf # - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf # - asset: fonts/TrajanPro_Bold.ttf
# weight: 700 # weight: 700
# #
# For details regarding fonts in packages, see # For details regarding fonts in packages, see
# https://flutter.dev/to/font-from-package # https://flutter.dev/to/font-from-package