Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
## [1.3.0] - 2026-09-22

@a-nisi-insideapp

- Fix a race between overlapping `fetch()` calls (e.g. the automatic fetch on mount racing a caller's own refetch triggered by async filter restoration): `GanttController` now serializes fetches instead of running them concurrently — at most one fetch is ever in flight, and any `fetch()` call that arrives while one is running is coalesced into a single follow-up run instead of starting a second one. This removes the need for callers to guard against stale responses overwriting fresher ones.
- A fetch listener that throws (a failing `activitiesAsync`/`holidaysAsync`, for instance) no longer leaves `GanttController` permanently unable to fetch, and no longer skips the remaining listeners. The failure is reported through `FlutterError.reportError` instead of being swallowed.
- Fix a `ConcurrentModificationError` when a fetch listener was added or removed while a fetch was in flight — for example a `Gantt` widget disposed while its data was still loading.
- Add an optional `style` to `GanttActivityAction`, applied to the action's `IconButton`, so callers can customize the button (e.g. its color on hover).

## [1.2.2] - 2026-04-28

@rickypid
Expand Down
8 changes: 4 additions & 4 deletions example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ packages:
path: ".."
relative: true
source: path
version: "1.2.2"
version: "1.3.0"
flutter_lints:
dependency: "direct dev"
description:
Expand Down Expand Up @@ -86,10 +86,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
url: "https://pub.dev"
source: hosted
version: "1.17.0"
version: "1.18.0"
nested:
dependency: transitive
description:
Expand Down Expand Up @@ -128,5 +128,5 @@ packages:
source: hosted
version: "2.2.0"
sdks:
dart: ">=3.9.0-0 <4.0.0"
dart: ">=3.10.0-0 <4.0.0"
flutter: ">=1.17.0"
4 changes: 4 additions & 0 deletions lib/src/classes/activity.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@ class GanttActivityAction {
/// Optional tooltip text for the action.
final String? tooltip;

/// Optional style for the action button.
final ButtonStyle? style;

/// Creates an activity action with an icon, tap handler, and optional tooltip.
const GanttActivityAction({
required this.icon,
required this.onTap,
this.tooltip,
this.style,
});
}

Expand Down
1 change: 1 addition & 0 deletions lib/src/widgets/activities_list.dart
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ class ActivitiesList extends StatelessWidget {
padding: EdgeInsets.zero,
onPressed: e.onTap,
icon: Icon(e.icon, size: theme.cellHeight * 0.8),
style: e.style,
);
return e.tooltip != null
? Tooltip(message: e.tooltip, child: child)
Expand Down
64 changes: 59 additions & 5 deletions lib/src/widgets/controller.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:async';

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';

Expand Down Expand Up @@ -164,13 +166,18 @@ class GanttController extends ChangeNotifier {
notifyListeners();
}

final List<VoidCallback> _fetchListener = <VoidCallback>[];
bool _fetchInFlight = false;
bool _fetchPending = false;

final List<FutureOr<void> Function()> _fetchListener =
<FutureOr<void> Function()>[];

/// Adds a listener to be called when data needs to be fetched.
void addFetchListener(VoidCallback fn) => _fetchListener.add(fn);
void addFetchListener(FutureOr<void> Function() fn) => _fetchListener.add(fn);

/// Removes a fetch listener.
void removeFetchListener(VoidCallback fn) => _fetchListener.remove(fn);
void removeFetchListener(FutureOr<void> Function() fn) =>
_fetchListener.remove(fn);

/// Removes all fetch listeners.
void removeAllFetchListener() {
Expand All @@ -180,9 +187,56 @@ class GanttController extends ChangeNotifier {
}

/// Notifies all fetch listeners to load new data.
///
/// If a fetch is already in flight, this does not start a second one in
/// parallel — it just marks that another fetch is needed and returns.
/// Once the running fetch completes, it immediately re-runs exactly once
/// more, picking up whatever changed in the meantime. This guarantees at
/// most one fetch in flight at any time, so a slower/older fetch can never
/// overwrite a faster/newer one — there is nothing left to race against.
///
/// A listener that throws is reported through [FlutterError.reportError]: it
/// neither skips the remaining listeners nor leaves the controller unable to
/// fetch again.
void fetch() {
for (var fn in _fetchListener) {
fn();
if (_fetchInFlight) {
_fetchPending = true;
return;
}
unawaited(_runFetch());
}

Future<void> _runFetch() async {
_fetchInFlight = true;
try {
// Iterate over a snapshot: listeners may be added or removed while this
// loop is suspended on an await (a Gantt widget being disposed mid-fetch
// removes its own listener), which would otherwise throw a
// ConcurrentModificationError.
for (final fn in List<FutureOr<void> Function()>.of(_fetchListener)) {
try {
await Future.sync(fn);
} catch (error, stackTrace) {
// A listener that throws must not abort the remaining listeners nor
// leave the controller wedged, but the failure still has to surface.
FlutterError.reportError(
FlutterErrorDetails(
exception: error,
stack: stackTrace,
library: 'flutter_gantt',
context: ErrorDescription(
'while running a GanttController fetch listener',
),
),
);
}
}
} finally {
_fetchInFlight = false;
}
if (_fetchPending) {
_fetchPending = false;
unawaited(_runFetch());
}
}

Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: flutter_gantt
description: "Flutter package for rendering an interactive and customizable Gantt chart widget."
version: 1.2.2
version: 1.3.0
homepage: https://github.com/insideapp-srl/flutter_gantt
repository: https://github.com/insideapp-srl/flutter_gantt

Expand Down
Loading
Loading