Skip to content

feat: opt-in workout import from Health Connect / Apple Health - #651

Open
EugeneSusla wants to merge 32 commits into
simonoppowa:developfrom
EugeneSusla:feature/health-workout-import
Open

feat: opt-in workout import from Health Connect / Apple Health#651
EugeneSusla wants to merge 32 commits into
simonoppowa:developfrom
EugeneSusla:feature/health-workout-import

Conversation

@EugeneSusla

Copy link
Copy Markdown

Closes #147. Also relevant to the broader health-sync discussion in #103.

Adds an opt-in import of workouts from Health Connect (Android) / Apple Health (iOS) as diary activities.

What it does

  • Settings → Health Connect / Apple Health (titled with the platform's own store name): an opt-in switch, plus a "calorie credit" slider (50–100%) applied to imported workouts only — manually logged activities are untouched.
  • Workouts are read on app start/resume (debounced) and via an explicit "Import now" button. Each imported workout becomes a regular activity on the correct diary day (respecting the configurable day boundary), raising that day's calorie/macro goals like any other activity.
  • Dedupe is by the platform record id, with a watermark plus a 24h overlap tolerance so late-syncing workouts still arrive and nothing imports twice.
  • Activity records keep provenance: the device-reported kcal is stored alongside the credited kcal, and the CSV export docs describe the new fields.
  • The slider's default credit is seeded from body composition when a body-fat reading exists (falling back to BMI, then a flat 72%). The rationale — additive energy-expenditure compensation — is cited on the in-app sources screen (Careau et al. 2021, Current Biology, DOI 10.1016/j.cub.2021.08.016, PMID 34453886; body-composition reference values from NCHS Vital and Health Statistics Series 11 No. 250).
  • Privacy section updated: access is read-only, opt-in, and nothing is written back to the health store.

Decisions maintainers should weigh in on

  • minSdk 24 → 26 (665ac92): forced by the Health Connect client library. This drops Android 7.x/8.0 support — flagging it explicitly since it's a user-facing floor change.
  • Three extra Android read permissions (a96b173, 8f0a1a2): besides exercise sessions, the manifest declares read access for total calories burned, distance, and steps. The health plugin fills a workout's totals by issuing those sub-reads per session, and Health Connect rejects the whole workout query with a SecurityException if any of them is not granted.
  • Source-matched energy attribution (6a6f10c): on Android, a session's calories are attributed from raw TotalCaloriesBurned records written by the session's own app, rather than the plugin's pre-summed total. The plugin sums every calorie record overlapping the session window regardless of writer, so an all-day background stream (Fitbit / Google Fit write basal+activity in 15-minute buckets — energy the TDEE-based goal already models) double-counts on top of the workout app's own figure; observed on-device as a 430 kcal gym session importing as 847 kcal. When the session writer logged no calories, the largest single-source sum in the window is used — never the cross-source total. iOS is unchanged (HKWorkout carries its own total).
  • HealthKit entitlement (3d0c06d): touches the Runner target's signing surface, which has consequences for release provisioning. Happy to revert cleanly if you'd rather wire the entitlement yourselves.
  • health pinned at ^13.1.4: 13.3.x pulls a win32 version that conflicts with package_info_plus ^9.

Notes

  • Imported kcal is whatever the device/app reported; the slider multiplier is the only adjustment, and it applies to imported workouts only.
  • Formatting: only new files are dart formated; pre-existing repo-wide format drift is left untouched (CI does not enforce format).
  • iOS is untested on hardware — I have no Mac, so the iOS path is verified by CI build only. The platform split keeps the HealthKit path identical to the plugin's stock behavior, but I'd appreciate a maintainer or community member exercising a real import on iOS.

Testing

  • Unit tests cover the import pipeline (budget/goal invariants, dedupe, watermark/window arithmetic, day-boundary filing, activity mapping, opt-in gating, background debounce, failure paths) and the Android energy attribution (same-source matching, cross-source exclusion, window edge semantics, fallback, unit conversion). Full suite green.
  • Verified on hardware: Pixel 9 with Health Connect fed by Fitbit + Hevy (permission grant/revoke flows, route-attached sessions, dedupe across repeated imports, credited kcal = reported × multiplier, and the double-count fix above).

Adds two nullable fields to UserActivityDBO and its entity: the platform
record id an activity was imported from, and the energy the exporting
device reported before any calorie-credit multiplier was applied. Both
are null for every manually logged activity, so existing rows read
exactly as they did before.

The external id doubles as the dedupe key for the workout importer, so
the update path carries it over rather than dropping it — losing it
would make an edited imported workout eligible for a second import — and
the data source gains a lookup for the ids already filed since a given
date.
Three new nullable config fields: whether workout import is enabled, the
share of a device-reported workout's energy that counts toward the daily
goal, and the watermark marking how far the last import read.

All three default to off / unset, so a config written before the feature
existed reads as opted out and no health data is touched until the user
says so. A stored multiplier outside the supported 0.50-1.00 range is
treated as corrupt and dropped rather than silently scaling every future
import by a nonsense factor.
Logging an activity is never just a box insert: burned calories raise
the day's goal ceiling 1:1 and the macro goals along with it, and a day
that has never been tracked has to be created first or there is nothing
to raise. The Quick Add sheet had that sequence copied out of
ActivityDetailBloc; a second programmatic writer would have copied it
again.

LogUserActivityUsecase now owns it. The day is passed separately from
the activity because an activity's own timestamp can fall on the other
side of a configured day boundary, so the caller decides which diary
column it belongs to. Quick Add's behaviour is unchanged.
…thKit

Adds the `health` plugin and puts the app's entire surface onto the
platform health store behind one abstraction, so nothing above the data
source layer ever loads a method channel — the plugin cannot run under
`flutter test`, and this is what keeps the import pipeline testable.

Two implementations: a thin wrapper over the plugin that asks for
read-only access to workouts and body fat percentage and hands back
plugin-free records, and a no-op that reports itself unavailable. The
locator probes once at startup and registers whichever works; a plugin
that fails to initialise is logged and falls back rather than taking
the app down with it.

The dependency is pinned below health 13.3.2: that release pulls
device_info_plus 13.2, whose win32 ^6 constraint collides with
package_info_plus 9's win32 ^5.
A watch reporting "480 kcal burned" is measuring the workout, not the
day: bodies claw a good part of that back over the following hours, so
crediting every reported calorie systematically overstates the budget.
How much is clawed back varies with body composition.

WorkoutCompensationCalc turns a body fat reading — or, failing that, the
user's BMI — into a suggested multiplier, interpolating between the two
ends of the gradient in Careau et al. 2021 (Current Biology 31(20),
PMID 34453886). Body fat percentiles come from the NCHS NHANES
1999-2004 DXA reference tables. Where the app is approximating rather
than quoting the sources, the doc comment says so and says why; a user
with neither input usable gets the sample-wide mean and nothing is
invented from thin air.
Finished workouts from Health Connect / Apple Health are filed as
ordinary activities: editable, deletable, and raising the day's calorie
and macro goals through the same path a manually logged workout takes.
What marks them is the platform record id, which is what stops the same
record being imported twice when the read window deliberately overlaps
the last one — fitness apps routinely sync a session hours after it
finished, and a window starting exactly where the previous ended would
miss those forever.

Details worth knowing:

  * a workout is filed under the logical day its start falls in, so a
    00:30 session with an 03:00 day boundary raises the previous day's
    goal, not today's (simonoppowa#139);
  * the credited energy is the device figure times the configured
    multiplier, while the raw figure is kept alongside it;
  * records with no energy, zero energy, or no duration are skipped
    rather than landing as noise;
  * known workout types map onto compendium activities for their name
    and icon — the MET value is never consulted, since the device's own
    energy figure overrides it — and unmapped ones become a Custom
    activity labelled with the platform's own type name.

Triggered at app start and on resume, both through a debounced entry
point that is inert until the user opts in and that logs rather than
propagates failures. The user-initiated path throws instead, so a
revoked permission can be surfaced rather than silently swallowed.
Read-only access to exercise sessions and body fat, requested lazily once
workout import is switched on. Adds the Health Connect package query, the
permission-rationale intent filter and the Android 14 permission-usage
activity-alias required by the health plugin, and hosts Flutter in a
FlutterFragmentActivity so the plugin can drive registerForActivityResult.
NSHealthShareUsageDescription only — the feature never writes back, so no
update description is declared.
HealthKit authorization is rejected without the entitlement. The App ID
needs the HealthKit capability enabled and the match profiles regenerated
before the next signed iOS build.
Sixteen keys covering the settings entry, the health sync sub-screen, the
imported-activity name and the energy-compensation source entry, in all
nine locales.
…reen

Careau et al. (2021) backs the suggested workout calorie credit. The entry
also records that imported workout energy comes from the recording app or
device rather than the in-app MET formula.
Workout import reads exercise sessions and body fat percentage, so the
blanket "no health-data access" claim no longer holds.
Completes the config chain for the two workout-import settings: the
loaded state now mirrors them like every other persisted preference, and
the setters delegate to AddConfigUsecase.
Opting in asks the platform for read access, and only on a grant does it
enable the import, seed the calorie credit from the user's body
composition and pull the backlog in. A refusal reverts the switch and
says what was refused rather than leaving a switch that looks on and
does nothing.

The credit slider moves in whole 5% steps between the bounds the config
entity defines, persists on release, and offers the computed suggestion
whenever it differs from what is stored. "Import now" reports how many
workouts it filed and refreshes the diary when it filed any. A device
without a health store still gets the row, and the screen explains why
nothing on it can be used.
Pins the behaviour that is easy to regress silently: the slider stays
inert and no permission is requested until the user opts in, a granted
opt-in seeds the suggested credit, a denied one leaves the setting off
and shows why, and the suggestion is only offered when acting on it
would move the slider.
The activity schema round-trips externalId and sourceReportedKcal (and
the older userKcal), none of which the sample record showed.
Unmapped workouts ended up named from the platform type string, so
activityImportedWorkout never gained a caller in any locale.
userKcal is set the moment the user edits an activity's calories,
manual or imported alike; only externalId and sourceReportedKcal are
import-only.
The health plugin fills a workout's totals by reading the session's
associated Distance, TotalCaloriesBurned and Steps records, and Health
Connect rejects the whole workout query with a SecurityException when
any of those reads is missing — the plugin then swallows the error and
answers with an empty list, so every import quietly filed nothing.
Request and declare the three extra reads on Android (HealthKit carries
the totals on the workout itself, so iOS keeps the two-type list), and
check the grants before reading so a revoked permission fails the
import loudly instead of advancing the watermark over a window that was
never actually read.
"Health Connect" / "Apple Health" is the name users recognize, so the
settings row and the screen use it directly instead of a generic
"Health sync" label; the subtitle drops the platform placeholder the
title now carries.
The plugin fills an Android workout's totalEnergyBurned by summing every
TOTAL_CALORIES_BURNED record overlapping the session window regardless of
which app wrote it. Alongside an all-day calorie stream (Fitbit and Google
Fit write basal+activity in 15-minute buckets) that inflates a real workout
by the background energy the calorie goal already models — a 430 kcal gym
session imported as 847 kcal on a device with a Fitbit stream running.

Read the raw calorie records instead and sum per source: the session
writer's own records when it wrote any, otherwise the largest single-source
sum in the window, never the cross-source total. Records belong to the
window by start time, matching Health Connect's between() semantics. iOS is
unchanged — HKWorkout carries its own total.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds opt-in Health Connect/Apple Health workout imports, calorie-credit adjustment, provenance storage, and automatic/manual synchronization.

Changes:

  • Adds platform health permissions, service integration, import/deduplication, and activity logging.
  • Adds health-sync settings, localization, compensation calculation, and sources.
  • Extends persistence/export models and adds comprehensive tests.

Reviewed changes

Copilot reviewed 51 out of 54 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
test/unit_test/workout_compensation_calc_test.dart Tests compensation calculations.
test/unit_test/import_workouts_usecase_test.dart Tests workout import behavior.
test/unit_test/health_package_service_test.dart Tests Android energy attribution.
test/unit_test/config_health_fields_test.dart Tests health configuration defaults.
test/features/settings/presentation/widgets/health_sync_screen_test.dart Tests health-sync UI.
README.md Documents health-data privacy.
pubspec.yaml Adds the health plugin.
pubspec.lock Locks health dependencies.
lib/main.dart Starts background import.
lib/l10n/intl_zh.arb Adds Chinese strings.
lib/l10n/intl_uk.arb Adds Ukrainian strings.
lib/l10n/intl_tr.arb Adds Turkish strings.
lib/l10n/intl_sk.arb Adds Slovak strings.
lib/l10n/intl_pl.arb Adds Polish strings.
lib/l10n/intl_it.arb Adds Italian strings.
lib/l10n/intl_en.arb Adds localization templates.
lib/l10n/intl_de.arb Adds German strings.
lib/l10n/intl_cs.arb Adds Czech strings.
lib/features/settings/settings_screen.dart Adds the health-sync entry.
lib/features/settings/presentation/widgets/health_sync_screen.dart Implements health-sync controls.
lib/features/settings/presentation/bloc/settings_state.dart Exposes health settings state.
lib/features/settings/presentation/bloc/settings_bloc.dart Persists health settings.
lib/features/home/home_page.dart Imports workouts on resume.
lib/features/add_activity/presentation/widgets/quick_add_activity_bottom_sheet.dart Reuses centralized activity logging.
lib/core/utils/url_const.dart Adds research URL.
lib/core/utils/locator.dart Registers health dependencies.
lib/core/utils/calc/workout_compensation_calc.dart Calculates suggested credit.
lib/core/presentation/sources_screen.dart Adds compensation research.
lib/core/domain/usecase/log_user_activity_usecase.dart Centralizes activity and goal updates.
lib/core/domain/usecase/import_workouts_usecase.dart Implements workout import.
lib/core/domain/usecase/add_config_usecase.dart Adds health-setting setters.
lib/core/domain/entity/user_activity_entity.dart Adds workout provenance.
lib/core/domain/entity/config_entity.dart Adds health configuration.
lib/core/data/repository/user_activity_repository.dart Exposes external-ID lookup.
lib/core/data/repository/health_import_repository.dart Wraps the health service.
lib/core/data/repository/config_repository.dart Persists health configuration.
lib/core/data/dbo/config_dbo.g.dart Updates generated config adapter.
lib/core/data/dbo/config_dbo.dart Stores health configuration.
lib/core/data/data_source/user_activity_dbo.g.dart Updates generated activity adapter.
lib/core/data/data_source/user_activity_dbo.dart Stores workout provenance.
lib/core/data/data_source/user_activity_data_source.dart Preserves and queries external IDs.
lib/core/data/data_source/health/noop_health_service.dart Handles unsupported platforms.
lib/core/data/data_source/health/health_service.dart Defines the health abstraction.
lib/core/data/data_source/health/health_service_factory.dart Selects a health implementation.
lib/core/data/data_source/health/health_package_service.dart Integrates the health plugin.
lib/core/data/data_source/health/external_workout.dart Models platform workouts.
lib/core/data/data_source/config_data_source.dart Adds health-setting persistence.
ios/Runner/Runner.entitlements Enables HealthKit.
ios/Runner/Info.plist Adds HealthKit usage text.
ios/Runner.xcodeproj/project.pbxproj Applies HealthKit entitlements.
docs/export-format.md Documents provenance fields.
android/app/src/main/kotlin/com/opennutritracker/ont/opennutritracker/MainActivity.kt Uses FlutterFragmentActivity.
android/app/src/main/AndroidManifest.xml Declares Health Connect access.
android/app/build.gradle Raises Android minimum SDK.
Files not reviewed (2)
  • lib/core/data/data_source/user_activity_dbo.g.dart: Generated file
  • lib/core/data/dbo/config_dbo.g.dart: Generated file
Suppressed comments (1)

lib/features/settings/presentation/widgets/health_sync_screen.dart:367

  • Project convention requires every new interactive widget to have a stable Semantics(identifier:) wrapper (AGENTS.md:158–179). Add an identifier to the action that applies the suggested multiplier.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +75 to +81
@override
Future<bool> requestPermissions() async {
return await _health.requestAuthorization(
_readTypes,
permissions: List.filled(_readTypes.length, HealthDataAccess.READ),
);
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 24b2ee9 — after requestAuthorization returns true, requestPermissions now re-checks hasPermissions on the workout read types and reports failure only on a definite false. null (iOS never reports read grants) still passes, matching the readWorkouts guard, which is unchanged. Body fat stays optional.

Comment on lines +135 to +136
@HiveField(33)
bool? healthImportEnabled;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3a5a40c — all the health fields (opt-in flag, multiplier, watermark, and the deleted-ids list added by 00fbd95) are now overlaid from the per-profile config box in _readMerged, in both branches, so each profile opts in independently, imports its own copy and keeps its own watermark; a fresh profile reads as opted out. They belong with the personal fields because the activity box they write into is itself per-profile.

Comment on lines +100 to +104
final workouts = await _healthImportRepository.getWorkouts(
from: from,
to: to,
);
final seenIds = await _userActivityRepository.getExternalIdsSince(from);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 508b442 — one shared in-flight future now covers the whole read → dedupe → write → watermark sequence. importNow joins an in-flight run instead of starting a second one (still returns the real count, still throws on failure), and importIfDue joins too rather than re-reading the platform the moment a run finishes. Serialization tests added; verified they fail with the guard removed.

Comment thread lib/main.dart Outdated
Comment on lines +80 to +83
// Pull in any workouts logged since the last run. Inert unless the user has
// opted in, self-debouncing, and it swallows its own failures — the app
// must never wait on, or fail to start because of, the health store.
unawaited(locator<ImportWorkoutsUsecase>().importIfDue());

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 325e343 — the bootstrap import is removed; the launch import now runs from HomePage initState through the same path as the resume import, so it reaches the same Home/Diary/Calendar refresh when something came in. Still non-blocking and failure-swallowing.

Comment on lines +157 to +163
_physicalActivityFor(workout, activityByCode),
// Mirrored into burnedKcal above because the aggregation layer sums
// that field; userKcal records that the figure is a measured one rather
// than something the MET formula produced, so editing prefills it.
userKcal: creditedKcal,
externalId: workout.id,
sourceReportedKcal: reportedKcal,

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9fee85d — an activity with an externalId no longer goes through the MET recompute: the credited kcal scales proportionally with the edited duration (half the time credits half the calories), on both burnedKcal and userKcal since the importer writes them as a pair. sourceReportedKcal stays exactly what the device reported and externalId is kept for dedupe. Chosen over a MET recompute because the device measurement is better information than the formula estimate.

Comment on lines +98 to +105
Future<Set<String>> getExternalIdsSince(DateTime from) async {
return _userActivityBox.values
.where(
(activity) =>
activity.externalId != null && !activity.date.isBefore(from),
)
.map((activity) => activity.externalId!)
.toSet();

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 00fbd95 — deleting an activity with an externalId now records the id in a per-profile tombstone list (a new config field), and the importer unions the tombstones into the dedupe set, so the deletion survives the overlapping re-read. The list grows only by user deletions, so it is kept indefinitely.

Comment thread docs/export-format.md
Comment on lines +122 to +127
Records also carry the optional `userKcal` (set once the user edits an
activity's calories, `null` before that), plus `externalId` (the health-store
record an imported workout came from, which is what stops a re-import
duplicating it) and `sourceReportedKcal` (the energy the device reported before
the workout calorie credit); the latter two are `null` on manually logged
activities, and all three round-trip on import/export.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2fc0891 — user_kcal, external_id and source_reported_kcal are now written and parsed (appended after the original columns; the header-driven parser still reads older CSVs without them), and the export-format doc table covers all three.

Comment on lines +337 to +341
IconButton(
icon: const Icon(Icons.info_outline_rounded),
tooltip: s.sourcesIconTooltip,
onPressed: _openSources,
),

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b59f16c — both buttons now carry Semantics identifiers (health-sync-sources, health-sync-apply-suggestion), the latter with container: true since it sits inside a layout-greedy Align.

Comment on lines +327 to +331
Expanded(
child: Text(
s.healthSyncKcalMultiplierLabel,
style: textTheme.titleMedium,
),

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b59f16c — the section title is now an AutoSizeText (maxLines: 1, minFontSize: 11, ellipsis) inside its existing Expanded.

Comment on lines +73 to +79
sources: const [
_SourceLink(
citation:
'Careau V, et al. (2021). Energy compensation and adiposity '
'in humans. Current Biology, 31(20):4659–4666.e2.',
url: URLConst.sourceEnergyCompensationCareau2021URL,
),

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b6ba26b — added Borrud LG et al. (2010), NCHS Vital and Health Statistics Series 11 No. 250 as a second source link under the energy-compensation entry, mirroring the citation already given in the calculator source.

pod install regenerated the lockfile for the new health and
device_info_plus (transitive) plugin pods; committed from the CI
runner's own pod install output, as no macOS machine is available
locally.
Android's requestAuthorization answers true as soon as any requested type
was granted, so a user who ticked body fat and left the workout records
unticked switched the import on with nothing importable behind it. Verify
the workout read types afterwards; body fat stays optional, and a platform
that will not report read grants (iOS) still counts as granted.
App start, every app resume and the settings screen's "import now" all
call into the same use case, and the read → dedupe → write → watermark
sequence has several awaits in it. Two overlapping runs each read the same
window, each find nothing filed against the record yet, and each write it:
one session logged twice, the day's calorie and macro goals raised twice.

Hold the run in flight and hand it to whoever arrives while it lasts, so
the sequence has one writer at a time. The manual path still gets a real
count back and still raises whatever the health store threw.
Bootstrap fired the import and dropped the result on the floor, so a
workout picked up at launch only appeared once something else happened to
reload the day. The resume path already refreshes the home, diary and
calendar-day blocs when anything came in — run the launch import from the
same place so it gets the same treatment. Still off the startup critical
path, and still failure-swallowing.
Editing an imported workout's duration ran it through the MET formula like
any other compendium activity, which replaced the device-reported energy —
the whole point of the import — with an estimate off a compendium value the
importer never consulted. Scale the credited calories with the duration
instead: half the time, half the credit.

sourceReportedKcal stays exactly what the device said, and externalId stays
put so the next import still recognises the record.
The merged config read only overlaid the nutrition goals from the active
profile's box, so the app box's copy of the health opt-in, the calorie
multiplier and the import watermark always won. Every profile therefore
shared one switch and one watermark — switching profile carried the opt-in
across, and whichever profile imported last moved the window for all of
them.

The activity box those imports land in is per-profile, so the settings that
drive them belong there too: overlay them from the profile box, and clear
them along with the other personal fields when there is no profile box to
read, so a fresh profile starts opted out with a watermark of its own.
The importer dedupes against the external ids of the activities on file and
deliberately re-reads a 24-hour overlap past its watermark, so deleting an
imported workout removed the only thing standing between that record and a
second import: the next run filed it straight back, goals and all.

Remember the external id of every imported activity the user deletes and
skip it on later reads. The list lives on the profile config next to the
rest of the health settings, so it is scoped the same way the activities
are, and it is kept indefinitely — dropping an entry resurrects the workout
it stands for, and it only ever grows by one entry per deletion.
The user_activity CSV stopped at the ten original columns, so userKcal,
externalId and sourceReportedKcal were dropped on export — an imported
workout came back from the round trip the docs promise as an ordinary
MET-derived activity, without the record id that dedupes it.

Append the three as optional columns and read them back. Appended rather
than inserted, and the parser is header-driven, so a CSV written by an
older build still parses with them null.
The sources button and the "apply the suggestion" button had no
accessibility identifier, so the per-branch UI verifier could not drive
them, and the section title was a plain Text sharing a Row with the
percentage and the button — a long localized label would have wrapped or
striped the row.

Give both buttons an identifier (with container: true on the one inside an
Align, whose bounds it would otherwise inherit) and let the title shrink to
fit inside its existing Expanded.
The energy-compensation entry credited only Careau et al. for the
calorie-credit suggestion, but the sex-specific body fat percentiles the
calculator interpolates between come from the NCHS body-composition
report, not that paper. List it alongside so the screen accounts for both
halves of the calculation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants