NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
pub.dev · #4284 most downloaded on pub.dev
A pure Dart toolkit for parsing, editing, validating, and converting workout activity files in GPX, TCX, FIT, GeoJSON, and CSV formats.
Last release 4 days ago
03 Oct 2026
Ships fairly regularly
a new release about every 4 weeks
Nearly every release is documented
notes for 24 of 24 stable releases
Nothing withdrawn
no release was ever pulled
11 months old
24 releases · first in 2025
…RawActivityBuilder . Old names still work (see Deprecated), so the upgrade is a drop-in unless you implemented one of the two interfaces under Breakin…
Facade cleanup: clearer, consistent names across ActivityFiles, RawEditor,
and RawActivityBuilder. Old names still work (see Deprecated), so the upgrade
is a drop-in unless you implemented one of the two interfaces under Breaking.
ActivityFormatParser and ActivityFormatEncoder are no longer exported; neither was ever an extension point, since parser and encoder selection switches on the ActivityFileFormat enum with no way to register an implementation.
ActivityParser.parse/ActivityEncoder.encode with the format you want; support for a new format ships in a release.ActivityFiles.import()/.importBatch() replace load/loadBatch.RawEditor.merge()/.splitBySport() and RawActivityBuilder.activityLabelNode()/.deviceNode()/.deviceSummaryNode() replace their ActivityFiles equivalents.RawEditor.autoFix() makes the auto-fix pipeline (sort/dedup, trim invalid points, recompute distance/speed, fill timestamp gaps, auto-lap-by-distance) a public chainable method, not just an option on convert().ActivityFiles.buildAndExport() builds and exports a RawActivity from raw location/channel streams in one call, the streaming counterpart to convert().EncoderOptions(geojsonGeometry: GeojsonGeometry.points) emits a Point FeatureCollection; add geojsonIncludeChannels: true to attach channel values per point.ActivityImportResult replaces ActivityLoadResult, matching the import() verb that returns it; the old name stays as a deprecated alias.ActivityFiles.convert() accepts diagnostics, merging caller-supplied entries into the result the way export() already did; this closes the last gap to the deprecated runPipeline.maxPayloadBytes: null now disables the buffer cap for stream sources instead of falling back to the 64MB default.Everything below still works as before; removal is planned for 0.10.0 with a migration entry then.
ActivityFiles.load/.loadBatch → .import/.importBatch.ActivityFiles.convertStream/.convertAndExportStream → convert, which accepts a Stream<List<int>> source and produces identical output; neither path parses incrementally, so the memory profile is unchanged.ActivityLoadResult → ActivityImportResult, via a type alias, so existing declarations and signatures keep compiling.ActivityFiles.merge/.splitBySport → RawEditor.merge/.splitBySport.ActivityFiles.gpxActivityLabelNode/.gpxDeviceNode/.gpxDeviceSummaryNode → RawActivityBuilder.activityLabelNode/.deviceNode/.deviceSummaryNode.ActivityFiles.normalizeActivity/.sortAndDedup/.trimInvalid/.crop/.smoothHeartRate/.recomputeDistanceAndSpeed → the equivalent ActivityFiles.edit(activity) chain.ActivityFiles.exportToCsv/.exportToGeojson/.exportToGeojsonPoints/.importFromCsv/.importFromGeojson → ActivityFiles.export/.import with an explicit format; the importFrom* methods are synchronous while import() is not, so use ActivityParser.parse(input, format) for a sync parse.ActivityFiles.exportToCsvMultiple → no exact replacement; it concatenates each activity's rows under one header in input order, while RawEditor.merge() plus export() sorts by time and drops points whose timestamps collide across activities, so encode each activity separately if you need every row.ActivityFiles.convertAndExport → convert for source-based calls, buildAndExport for location/channels-based calls.ActivityFiles.runPipeline and ActivityExportRequest → convert for the fromSource/fromStream factories, export for fromActivity.ActivityConverter → ActivityFiles.convert, or ActivityParser.parse plus ActivityEncoder.encode for a synchronous conversion; it had no capability of its own.RawTransforms.computeCumulativeDistance → RawEditor.recomputeDistanceAndSpeed(), reading the total from the last Channel.distance sample; RawTransforms.resample is unaffected.ParseFidelityMode → normalize: false on convert/export/exportAsync; the enum was never wired into the pipeline and has no effect.FitCorruptionHandling and the fitCorruptionHandling parameter → strictFitIntegrity, which has the same effect.IntegrityStats.invalidMessages/.recoveryAttempts/.formatSpecificIssues → no replacement; no parser ever populated them.One column per month.
GeoJSON features with no timestamp data no longer get an unmarked synthetic epoch fallback time; a diagnostic reports it now.
gpxWaypoints to a non-GPX format now reports lossy.waypoints_dropped instead of silently producing a near-empty file.FIT no longer nulls out a legitimate max-range value (e.g. a calories/power reading of 255) that collides with a narrower type's sentinel.
<trkseg> when two source segments overlap in time.<rte>) encoding no longer drops non-standard child elements like a route's <link>.<Activity>'s distinct notes/creator/device info in a multi-activity file.ns3 foreign extension namespace to Garmin's schema.creator with device.model on round-trip.NaN/Infinity/-Infinity as valid numeric values.altitude overwrite the real point elevation.RawActivity.flattened() no longer drops summary/device/notes/waypoints/routes/metadata carried on additionalTracks.RawActivity.approximateDistance no longer assumes an unsorted distance channel is already in time order.validateDeviceMetadata no longer fires a redundant second diagnostic for an already-reported blank manufacturer.FitTypedActivityView.developerChannels now exposes every non-native FIT channel, not just running_power/fit_dev_-prefixed ids.RawEditor.ensureStrictTimeOrder() no longer leaves a lap's startTime after its own endTime when a tie-break nudge pushes it past.GPX trackpoints missing <time> no longer drag the segment's lap bounds to the epoch fallback.
<time> no longer drag the segment's lap bounds to the epoch fallback.StartTime postdates it.validateLapBoundaries() no longer skips corruption checks on activities with no GPS points.activity_type is no longer locked to whichever feature parses first.additionalTracks when the first track feature is empty.IntegrityMode.silent no longer discards every diagnostic, only integrity ones.maxPayloadBytes above 64MB no longer silently empties ActivityLoadResult.payload.merge() no longer skips normalize: true for a single-activity list.EncoderOptions is no longer a silent no-op for CSV/GeoJSON export; CSV lat/lon/elevation now default to fixed precision instead of raw toString() output.elevation, latitude, etc.) corrupt the real field on round-trip.lat/lng/ele/hr, not just exact spellings.splitBySport() no longer drops points in a gap between two laps of the same sport.trimToPoints(); they suggest RawEditor.crop() instead.FIT parsing no longer mistakes vendor-specific messages for GPS records, producing garbage coordinates/altitude/heart rate on real files; messages it
<time> are no longer dropped; they keep an epoch timestamp instead, now reported via a diagnostic like an invalid timestamp already was.<Track> (pause/resume) no longer read only the first one.load()/convert()/convertAndExport() no longer crash on a Stream<List<int>> source with isolates on (the default).ActivityConverter.convert() no longer drops repair diagnostics.trimInvalid() no longer drops laps on activities with no surviving GPS points (indoor/trainer).crop()/deleteRange()/insertPause()/removePause() now carry events/lengths/sets forward, like shiftTime() already did.sortAndDedup()/updatePoint() no longer pick a random winner among duplicate timestamps; sorting is stable now.splitBySport() no longer bleeds points between sports on brick workouts (run/bike/run); splitting is per-lap now.flattened() no longer gives every lap the primary track's sport; each lap keeps its own now.coordinateProperties.channels, and MultiLineString parsing now collects it per line too.gpxtpx: tags now.lossy.channels_dropped instead of dropping a channel with no warning.additionalTracks now.coordTimes (togeojson/Mapbox), not just our own coordinateProperties.times.start_time/total_elapsed_time or a corrupted/reordered timestamp no longer get dropped or produce a negative duration; a start is inferred, or the duration is clamped to zero, with a diagnostic either way.convert()/export() no longer only normalizes/orders the primary track: multi-track sources are flattened first for single-track formats now, and GPX normalizes additionalTracks directly instead of leaving them untouched.total_calories/total_steps/device_manufacturer are no longer dropped on parse.exportAsync(useIsolate: true) no longer drops most of RawActivity crossing the isolate boundary.
exportAsync(useIsolate: true) no longer drops most of RawActivity crossing the isolate boundary.normalize: true no longer skips sentinel repairs on already-sorted input.splitBySport() no longer duplicates a point sitting exactly on a shared lap boundary.trimInvalid() no longer drops GPX extensions/attributes when clearing a sentinel elevation.avg_heart_rate: 0 for laps with no heart rate data.convert()'s isolate branch no longer exports the pre-autoFix activity.GeoJSON LineString export no longer fabricates total_calories / total_steps as 0 .
total_calories/total_steps as 0.geojson.point.*, csv.row.*) instead of silently dropping malformed points/rows, with correct per-feature diagnostics and CSV row numbers.ActivityFiles.channelSamplesFrom() emitted second-resolution timestamps instead of milliseconds, which didn't match what builderFromStreams() / conver
ActivityFiles.channelSamplesFrom() emitted second-resolution timestamps instead of milliseconds, which didn't match what builderFromStreams()/convertAndExport() expect — round-tripping through it silently produced dates near 1970 instead of the correct time. If you built on this in 0.7.0/0.7.1, re-check any stored output.convertAndExport(location: ..., autoFix: ...) silently ignored autoFix when building from raw streams instead of a source file.No changes to the package itself
FIT parsing is now remapped to the official FIT profile. Since 0.6.0, session/lap stats and the sport value were parsed incorrectly
<trk> becomes the primary RawActivity; the rest are stored in RawActivity.additionalTracks. Single-track encoders (TCX/FIT/CSV/GeoJSON) merge them automatically via the new RawActivity.flattened(), so conversions keep every point. But activity.points after loading a multi-track GPX now contains only the first track.RawEditor.trimInvalid() (run by convert/export when normalize: true, the default) removes Null Island points (lat/lon ≈ 0) and clears elevations ≤ −499 m while keeping the point. Repairs are reported via RawEditor.repairDiagnostics and in conversion/export diagnostics (repaired.sentinel_coords_removed, repaired.sentinel_elevation_cleared). Pass normalize: false to opt out.ValidationResult.isValid / LapValidationResult.isValid now derive from the structured diagnostics list as well as the legacy errors list.Parsers and encoders now round-trip everything a format can hold; anything the target format can't represent is reported via a lossy.* diagnostic (lossy.sets_dropped, lossy.events_dropped, lossy.lengths_dropped, lossy.sessions_dropped, lossy.summary_dropped, lossy.laps_dropped, lossy.multi_track_flattened) instead of being dropped silently. Per format:
extraFitFields/extraFitArrays) are all preserved; developer fields decode using their declared name, type, scale, and offset.gpxWaypoints, gpxRoutes) instead of being folded into the track; multi-segment tracks and unknown extensions round-trip.ns3:TPX Watts/Speed/RunCadence) now parses as channels instead of being dropped; notes, author, and lap intensity/trigger fields round-trip.New data model and API:
ActivitySummary.poolLengthMeters/.numActiveLengths/.swimStroke/.avgStrokeCount, Lap.numActiveLengths/.swimStroke, the SwimStroke enum, plus ActivitySummary.subSport and .totalCycles.RawActivity.sets as WorkoutSet (type, repetitions, weight, exercise category; raw id kept for round-trips).RawEditor: insertPoint, deletePointAt, updatePoint, deleteRange, insertPause, removePause, all with lap/set boundary clipping; shiftTime now also shifts sets.ActivityFiles.loadBatch(sources) with per-file error capture, onProgress, and stopOnError.ValidationDiagnostic (stable code, suggestedFix, priority) on every validation result; ParseDiagnostic gains suggestedFix/priority; new validateDeviceMetadata() and validateChannels() validators; DiagnosticCategory documents the code prefixes.ActivityFiles.channelSamplesFrom() (channels → export-ready stream samples), Lap.copyWithoutSport(), ValidationResult.fromDiagnostics()/LapValidationResult.fromDiagnostics(), and the ParseFidelityMode enum (declared for 0.8.0; this release always uses pragmaticNormalize).gpx.wpt.invalid_timestamp/gpx.rtept.invalid_timestamp now say the point is kept with an epoch fallback time (the previous "ignored" wording contradicted the behaviour; codes unchanged).product_name is now captured as ActivityDeviceMetadata.model.atemp/wtemp were rounded to whole numbers although the schema types them as decimal (18.5 °C came back as 19).ActivityFileFormat now includes csv and geojson ; exhaustive switch statements must handle the new enum values.
ActivityFileFormat now includes csv and geojson; exhaustive switch statements must handle the new enum values.runValidation: true). Set runValidation: false to keep previous behavior.FitCorruptionHandling and ActivityAutoFixOptions.asFitView() typed accessors for session/lap/record data. (lib/src/fit/typed_views.dart)autoLapByDistance option generates laps by distance with sport defaults. (lib/src/api/pipeline_options.dart, lib/src/api/activity_files_facade.dart)ActivityFileFormat path (detectFormat, load, convert, export, parser/encoder routing, CLI options).validateRawActivity() and RawEditor.validateLapBoundaries().autofix.laps.auto_generated is emitted when laps are generated. (lib/src/api/activity_files_facade.dart)fit.record.recovered_timestamp) instead of returning empty output.timestamp instead of runtime-dependent DateTime.now().ActivityFiles.splitBySport() now preserves lap metadata fields (e.g. avg/max metrics, calories, FIT event fields) while removing per-lap sport in split outputs.strictFitIntegrity now throws the same detailed troubleshooting FormatException across direct load and stream pipeline paths.Changed Upgraded dependencies Minor Fixes
Upgrading? This version mitigates the 0.4.0 breaking changes: the 64MB payload limit is now configurable per-call via maxPayloadBytes and file paths w…
maxPayloadBytes and file paths work with allowFilePaths: true or by passing File objects.exportToCsv(), importFromCsv(), exportToGeojson(), exportToGeojsonPoints(), importFromGeojson().wtemp, depth, speed, course, bearing) plus existing v1 fields (hr, cad, power, atemp) with corresponding channel types and ChannelSnapshot accessors.Lap.sport field enables per-lap sport tracking, and ActivityFiles.merge()/splitBySport() combine or divide multi-sport activities.ActivitySummary and additional Lap fields; extra record fields are surfaced as custom channels.scripts/generate_artificial_fixtures.dart) that refreshes sample fixtures for tests and examples.RawEditor.validateLapBoundaries() for detecting timing mismatches.\r\n → \n).Lap class includes optional sport field; null values inherit from activity-level sport.doc/testing.md for details.maxPayloadBytes parameter (default: 64MB).fit.data.unknown_definition, fit.no_usable_data.Synthetic test data generator with clean fixtures
Fix FIT parser to remove corrupted data points with invalid timestamps or coordinates, ensuring FIT→GPX and FIT→TCX output matches reference files
Fix FIT parser for non-standard message ordering. Thanks @hallr-boulder for reporting (fixes #2)
Fix formatting and prevent it from happening again with a local git hook
Plain string sources are always treated as inline payloads; pass a File or set allowFilePaths: true on load/convert/convertAndExport/ ActivityExportRe
File or
set allowFilePaths: true on load/convert/convertAndExport/
ActivityExportRequest.fromSource to read from disk.ActivityFiles.defaultMaxPayloadBytes) applies to
load/convert/detect and streamed pipelines; oversized inputs throw or emit
error diagnostics. Stream from disk/network or split files to go larger; only
ActivityParser.parseStream(maxBytes: ...) lets you override the limit.ActivityLoadResult.bytesPayload exposes
buffered bytes even when the source was a Stream<List<int>>.strictFitIntegrity: true); the encoder now emits
invalid coordinate sentinels for sensor-only activities.EncoderOptions and CLI flags can emit GPX 1.0/1.1
and TCX v1/v2; defaults remain GPX 1.1/TCX v2. FIT remains core-workout only.gpxVersion is GpxVersion.v1_0.RawTransforms and RawEditor live in dedicated modules
(transforms/raw_transforms.dart, transforms/raw_editor.dart); transforms.dart
still exports both for existing imports.ChannelMapper.cursor exposes reusable per-channel cursors; mapAt now wraps
it so overlays reuse cached lookups.RawTransforms.resample/_resampleNearest fast on long recordings.RawEditor skips work on sorted/valid inputs and reuses its timestamp cursor
during downsampleTime.asBytes() when FIT sources arrive as
base64 strings.RawEditor.smoothHR now uses a sliding window to stay O(n).RawEditor.downsampleDistance keeps the final point even on short hops or
duplicate timestamps, preserving distance/duration and channel alignment.RawEditor.markLapsByDistance recovers from non-monotonic distance channels
(e.g. pause resets) to keep splits accurate.RawEditor.smoothHR respects even-numbered windows instead of averaging an
extra sample.normalize: false auto-sort/dedup when needed; recomputeDistanceAndSpeed
also self-sorts to avoid invalid speed/distance.RawEditor.sortAndDedup clones lists before sorting to keep prior
RawActivity instances immutable.ParseDiagnostic errors instead of raw exceptions.parseStream, convertAndExportStream, runPipeline with
streams) returns diagnostics for malformed/oversized payloads instead of
throwing.convert honors explicit --encoding, reads GPX/TCX as bytes to avoid
Latin-1 corruption, and exits non-zero on parser errors.ActivityFiles.runPipeline no longer runs validation twice when
runValidation is enabled.RawActivity.copyWith keeps collections immutable, recognizes canonical
inputs to avoid clones, and reuses cached distances.RawTransforms.resample sorts points before resampling to avoid RangeErrors
and keep start/end ordering.ActivityFiles.convert enforces the export ordering guard when
normalize is false; detectFormat no longer probes filesystem paths
unless allowed.### Fixed - wrong version in pubspec.yaml
ActivityFiles.export produces encoded payloads with optional validation, normalization, and aggregated diagnostics in a single helper.
ActivityFiles.export produces encoded payloads with optional validation, normalization, and aggregated diagnostics in a single helper.ActivityFiles.convertAndExport exposes the same workflow directly from raw sources, including optional validation.ActivityFiles.exportAsync and convertAndExportStream mirror the export workflow off the UI thread and for streamed payloads.ActivityExportRequest and ActivityFiles.runPipeline let callers describe parse → normalize → export pipelines with consistent isolate controls.warningCount, hasErrors,
diagnosticsSummary, etc.) are now available on load, conversion, and export results to simplify UI surfacing.ActivityProcessingStats and NormalizationStats capture normalization and validation metrics alongside export diagnostics.sortAndDedup,
trimInvalid, smoothHeartRate, crop), structural validation, and channel snapshots.RawActivity and RawActivityBuilder gained device metadata and namespace aware GPX extension support, enabling richer encoder output without custom glue.DiagnosticsFormatter provides reusable helpers for summarising ParseDiagnostic collections across logs and UI surfaces.builderFromStreams,
convertAndExport with location/channels) accept timestamp/value tuples
so backends can export without manual model translation.ActivityFiles.registerSportMapper supplies pluggable sport inference and
ships string/ID heuristics for common wearable categories.gpxActivityLabelNode, gpxDeviceSummaryNode) for custom
extensions with automatic namespace registration.ActivityDeviceMetadata.ActivityFiles.convert and convertAndExport now accept exportInIsolate, bringing isolate offloading parity while ensuring FIT byte caches refresh when the encoded payload changes.RawEditor.markLapsByDistance now reports per-split distances correctly and
always emits a trailing partial lap when applicable, ensuring summary totals
stay accurate.RawEditor.downsampleTime keeps the final point in the activity and performs
sample matching without quadratic scans, avoiding data loss on closely spaced
tracks.ActivityFiles.channelSnapshot now uses binary search when resolving channel
samples, reducing lookup cost from O(n) to O(log n) for large time-series
streams.ActivityParseResult.warnings remains available but now forwards to the new structured diagnostics; it is marked deprecated to encourage migration.
ActivityParser.parseAsync,
parseBytesAsync, and parseStream optionally offload work to isolates for
smoother UIs and streaming IO.ActivityFiles facade providing ergonomic load, convert, and edit
helpers tailored for app integrations.RawActivityBuilder for incremental creation of activities.ActivityFiles.load, detectFormat,
and conversion flows with real GPX/TCX/FIT fixtures.ActivityParseResult.warnings remains available but now forwards to the new
structured diagnostics; it is marked deprecated to encourage migration.ActivityConverter.convert still accepts the warnings parameter, which is
deprecated in favor of the richer diagnostics sink.### Fixed - pub.dev score fix.
### Fixed - Fix README.
Handle FIT compressed timestamp headers and ensure unknown message types advance the reader instead of hanging.
ActivityParser.parseBytes, broaden ActivityConverter.convert input
support, and let the CLI operate on raw FIT binaries without manual base64.Your coding agent can read these notes before it upgrades. Set up the MCP server →