Skip to main content

hintful 1.0.0 copy "hintful: ^1.0.0" to clipboard
hintful: ^1.0.0 copied to clipboard

Onboarding tours and tooltips for Flutter. Registry-based targets, theme-aware coach marks, smart positioning, accessible by default — no overlay until a tour starts.

Changelog #

1.0.0 #

The first stable release. The 1.x API is frozen: the supported import is the single package:hintful/hintful.dart, and every type documented as "closed in 1.x" (HintSkipReason, TooltipPosition, FocusShape, HintMissingTargetPolicy, HintTourOfferResult, HintStore, HintMarkPolicy) will not grow before 2.0.

Upgrading from 0.7.0 — the whole migration in one list.

Breaking — public surface #

  • One import. lib/engine/ and lib/widgets/ moved under lib/src/; deep imports (package:hintful/engine/..., package:hintful/widgets/...) no longer resolve. Every barrel export names its symbols, so a new public class can no longer leak in by accident.
  • Content is the constructor path. HintStep/HintAdditionalTooltip lost the flat title/description/titleBuilder/descriptionBuilder params and the matching getters — pass content: HintStepContent(...) and read step.content.title. JSON keeps the flat title/description keys.
  • No rect targets. HintStep.targetRect, the overlay: constructor parameter, HintOverlayProvider and the machine's rect branches are gone — every step anchors to a registered HintTarget.
  • Tap config is one sealed type. tapOnTarget/onTapTarget and the overlay pair collapse into HintTapBehavior per region: targetTap/overlayTap with advance() / ignore() / custom(onTap). The JSON wire keeps the historical bools (true ⇔ advance).
  • HintTourOfferResult is six-valued. declined splits into declined (this dialog), versionGated (already ran for this version), previouslyDeclined (the offer was declined before), busy (accept while another tour is running) and nothingToShow (accepted, but no valid steps left).
  • Removed: HintTour.fromEnum; the factory trio (HintTourFactory/InMemoryHintTourFactory/FetcherHintTourFactory — use HintTour.fromJson + your own HTTP client); the adapter stubs; the DebugPrintDiagnostics class; the animation presets (HintEntryAnimation, HintStep.transition, hintTransitionDuration) — entry animation is the tooltipBuilder's job now; and two entry points that only restated others: restart (an idle-checked finish + showTour, and it marked a store-gated tour shown as a side effect) and tryShowHint (never referenced — tryShowTour covers the quiet guard).
  • The entry points are one show* family. start → showTour, tryStart → tryShowTour; show-once is no longer a second method but tryShowTour(tour, mark:) — an omitted mark means "the store is not consulted", so tryStart(tour) is exactly tryShowTour(tour). HintTourOfferResult.started shown (it now pairs with versionGated/previouslyDeclined).
  • HintController.currentState is gone. Read controller.state.value (controller.state.value.isIdle, or the isIdle shorthand) - one source of truth, so state and its current value can never disagree.
  • showTour/showHint return Future<bool>. true means the tour went on screen; the Future completes when it starts, not when it ends (HintTour.onExited observes the end). showHint now takes mark: and runs through tryShowTour, so it returns false instead of asserting when a tour is already running.
  • HintSkipEvent.tourId/targetId are String? (null = unknown - the '?' sentinel is gone) and the event has value semantics (==, hashCode, toString).
  • HintActions gained goTo(int). Implementations of the contract must add it; the three tap-behavior subtypes (HintTapAdvance/HintTapIgnore/ HintTapCustom) are exported so a switch over the sealed family is exhaustive.
  • Renames: HintStep.waitTimeout → stepTimeout; moreTargets → additionalTargets; moreTooltips → additionalTooltips (the JSON wire keys rename with them); HintTooltip → HintAdditionalTooltip (it only ever appears in additionalTooltips); compareVersions → compareHintVersions (one package-prefixed name in the barrel; hintful_prefs re-exports the new name); onBeforeAction/onAfterAction → onStepEnter/onStepExit; HintSkipReason.targetNotRendered → overlayUnavailable.
  • JSON wire: the step timeout key is stepTimeoutMs, the same name the tour default uses (it was waitTimeoutMs, which still parses — a step written as {"stepTimeoutMs": …} is no longer silently ignored).
  • Internalised (leave the barrel): the render contract (HintOverlayHost, defaultOverlayHost, the position types), the register-path, the inheritance/scope helpers, the diagnostics helpers and kHintFocusPadding.
  • HintStore is frozen at two members. shouldShow/markShown are the whole contract; clear() is a concrete store's own dev tool (the shipped hintful_prefs store has one).

Breaking — behaviour #

  • The store is app-wide. The store: parameters and the HintController.store field are gone — configure once with Hintful.configure(store: ...). Without one, a session-scoped InMemoryHintStore keeps show-once working for this run (debug prints a one-time warning).
  • Headless is a test seam. HintController() always renders; the @visibleForTesting factory is HintController.test() — always headless, with an internal host: seam for the package's own overlay tests.
  • HintMarkPolicy replaces markOnFinish (on tryShowTour(mark:) and showHintTourOffer): onAnyExit (the default — finish, skip and timeout all count), onFinish, manual.
  • skipStep is the tour default (was abortTour) and is tour-level only — the per-step field is gone. The JSON default is aligned: an absent missingTargetPolicy now parses as skipStep, matching the Dart default and the documented contract.
  • HintAdditionalTooltip.position defaults to TooltipPosition.auto (was required).
  • Step hooks bracket a visit: onStepEnter/onStepExit fire once per visit, serialized, with old.exit before new.enter (target vanish/reappear does not re-fire enter).
  • Diagnostics are a plain function (HintDiagnosticsHandler), one per controller; debug builds always print the line before invoking it.

Added #

  • Hintful (app-wide configuration), CallbackHintStore (a store in three lines over your own storage), the session InMemoryHintStore fallback and mark: on tryShowTour (show-once from the box).
  • compareHintVersions — the segment-wise version ordering the stores gate on ("1.10.0" > "1.9.0"), now public so an app-side HintStore shares the exact rule; hintful_prefs re-exports it.
  • HintStore.shouldShowVersion — the gate rule itself (never shown → show; no minVersion → once ever; last shown older than minVersion → show again) as one public static, called by InMemoryHintStore, CallbackHintStore and hintful_prefs: an app-side store delegates to it instead of restating the rule. A static on the exported HintStore, not a third interface member — the 1.x contract stays two.
  • HintStepContent, HintTapBehavior, HintTheme.tourOfferLabels, HintTarget(focusShape/focusPadding), the withHint sugar, autoScroll, the multi-content additionalTooltips, and structural JSON validation (FormatException on a missing/empty id/steps/targetId).
  • HintTheme.holeRadius - the corner radius of a rounded-rect spotlight hole (12, until now hardcoded inside the painter): the pulse ring reads the same value so ring and hole stay one shape, and it is clamped to half the shortest side so a tiny target cannot invert itself.
  • hintful_prefs 1.0.0 — the new companion package: a shared_preferences-backed HintStore (namespaced keys, clear() dev tool, public compareHintVersions). The core stays dependency-free.

Fixed #

  • tryShowTour reports what happened, not what was attempted: a tour showTour declined to run (every step stripped as a typo in release, an empty tour) now returns false instead of true. Callers that arm a mark on the return value no longer record a tour that never showed.
  • Diagnostics tell the truth: overlayUnavailable is reported once per tour (not once per state change), a busy showTour no longer emits phantom unknownTarget events, typo filtering preserves tour-level fields, and a throwing step hook is logged in release too.
  • showHintTourOffer records an accepted tour through tryShowTour(mark:), keeps its decline keys namespaced apart from the tour's own key, and takes an optional pageId (defaults to tour.id).
  • HintTheme.copyWith and HintTooltipLabels.copyWith are fully typed: the Object? sentinels are gone, so copyWith(tooltipTitleStyle: 'x') no longer compiles. An omitted or null argument keeps the current value (the rule every field follows); the three nullable overrides are dropped with withoutImageFilter(), withoutTitleStyle(), withoutDescriptionStyle() and withoutAnnounceStep().
  • The focus padding is part of the hole everywhere. The scrim already cut the inflated rect, but tooltip placement, the tail and the tap regions used the raw target bounds — with focusPadding > 0 the tooltip crowded the clear ring, the tail aimed inside it and a tap in the ring counted as an overlay tap (advancing the tour when overlayTap: ignore() was set). Scrim, blur clip, placement, tail and tap regions now share one hole list.
  • The pulse ring matches the hole it rings. It drew its own shape table (a radius-4 rectangle, an unclamped radius-12 rounded rect) instead of the scrim's holeShape, and shouldRepaint ignored focusShape/focusPadding — a step change kept the previous step's ring.
  • The missing-store fallback warns in release too: the session InMemoryHintStore message is no longer debug-gated, so a storeless release build says why "show once" keeps showing.
  • Version floors are honest: Dart ≥ 3.2 / Flutter ≥ 3.16 — the single newest API the package touches is MediaQuery.textScalerOf. The previous claim (3.0 / 3.10) would not have compiled for a consumer on 3.10–3.15.

0.7.0 — honest presets, tolerant JSON, tighter surface #

Breaking: HintSkipReason.targetUnmountedDuringStep is gone — drop the case from an exhaustive switch — and four engine symbols leave the public barrel.

  • HintCurve.easeOut is a real preset: fade + scale 0.96 → 1 on Curves.easeOut, 200 ms. It used to behave like "no animation".
  • fromJson tolerates an unknown enum name: it falls back to the field default and is reported through the new onWarning callback (also on FetcherHintTourFactory) instead of throwing.
  • Exported hintTransitionDuration — the reduce-motion helper the presets use, so a custom tooltipBuilder shares the same contract.
  • Duplicate targetId warnings now actually print in debug builds; onWarning still fires.
  • autoScroll no longer flashes the tooltip in a screen corner: while the target is still off screen the tooltip waits at the edge it is coming from and rides the scroll onto it. The position watch dropped the tooltip for the whole animation — an unpainted target has no compositor transform — and the placement parked it in the top-left corner.
  • Removed findClosestTargetId; formatHintSkipped, editDistance, CompositorHintResolver and UnpositionedHintResolver are no longer exported. They stay public inside lib/engine/ for the package's own tests, and custom hosts keep HintPosition, PositionedHint, UnpositionedHint and HintPositionResolver.
  • Dropped the stale hintful_bloc/ references from .pubignore and analysis_options.yaml.

Docs: README rebuilt around badges, a demo section and the withHint/l10n paths; doc/best_practices.md now covers 0–21 with an index; the FAQ grew to ten answers.

0.6.2 — autoScroll, withHint, target focus and l10n #

  • autoScroll on HintTour/HintStep (opt-in, false by default) — brings offscreen targets into view.
  • HintTarget(focusShape/padding) as target default — HintStep overrides; no per-step duplication for round icons.
  • withHint extension: child.withHint('id') sugar over HintTarget.
  • titleBuilder/descriptionBuilder on HintStep/HintTooltip — l10n via BuildContext without threading it through AppTours.
  • Example: Step scroll (per-step autoScroll), L10n (builder), withHint on filter-all, entry-5 circle, intro autoScroll: true, full custom TweenAnimationBuilder + float.

0.6.1 — flicker-free spotlight #

  • First-frame seed from RenderBox — no white flash on start/step change.
  • Global scrim — no bottom gap on scroll.
  • Scroll-synced holes — scrim + tooltip move with content in same frame.

0.6.0 — spotlight correctness (saveLayer scrim, rect targets, honest states) #

  • First-frame content: positions seed synchronously from the targets' own render objects (initState + step changes), so dim + hole + tooltip render on the very first frame — no normal-UI flash on start, no stale step on transitions. Scroll-driven motion follows the compositor synchronously (ancestor Scrollables observed) — no one-frame lag while the screen scrolls. Live compositor resolvers upgrade in behind (same values ± subpixel).
  • One scrim mechanism for every shape: fullscreen dim + BlendMode.clear holes in an isolated layer (no boolean geometry, overlap-correct); blur clips to one even-odd path. Removed the strips path (scrimStrips, pre-1.0 breaking note).
  • First shown step renders dim + hole together with the tooltip (was: tooltip without dim until a step change).
  • targetRect steps: immediate Active, static spotlight with tooltip; explicit overlay provider for zero-target tours.
  • Offscreen/culled targets: no frozen spotlight — retracted while unpainted, remounted on return; bringing targets into view stays the app's job. No stale tooltip on step change (transition frame unmounts).
  • Single-step hints keep no action row (no meaningless Done).
  • Hardened holes: over-shrunk padding degrades to full dim, corner radius clamped, never throws.
  • DRY: one hole geometry, one tooltip content/placement/entry source, FocusShape end to end, one machine step-entry path.
  • Accessibility: sprung honors reduce-motion; offer dialog awaits start.

0.5.0 — production hardening (l10n, skip-missing, safe start, scopes) #

Four battle-feedback fixes, all backward-compatible:

  • Localization hook: HintTooltipLabels (engine/labels.dart) — skip/ back/next/done, waiting placeholder preparing, screen-reader announceStep. Wired as HintTheme.tooltipLabels (English default) + a per-tooltip DefaultTooltip(labels:) override — localize once in the theme instead of duplicating the tooltip layout per language. The waiting-phase "Preparing…" comes from the labels too.
  • Missing-target policy: HintMissingTargetPolicy.skipStep vs abortTour (default) — on HintTour (incl. fromEnum, JSON) with a per-step override. A timed-out step is diagnosed (timeout) and the tour continues; skipping the last step finishes normally. Pair with a short waitTimeout (Duration.zero skips instantly, no waiting flash). New machine effect StepSkippedEffect.
  • Safe start: isIdle, tryStart (false when busy — no assert, no state change), restart (silently replaces the running tour), tryShowHint. start keeps its debug contract.
  • Controller scopes: scopePrefix isolates tabs/split-view sharing one registry — foreign ids neither activate steps nor count as typo candidates.

0.4.0 — server-driven tours + adapters #

  • HintTour/HintStep/HintTooltip now fromJson/toJson (specs.dart:194) — steps are title/description + position/moreTargets/moreTooltips, tooltipBuilder stays code-side. FetcherHintTourFactory (engine/tour_factory.dart) takes your fetcher (Uri)=>Future<String> — no http dependency in hintful.
  • Adapters lib/src/adapters/{bloc,riverpod,provider,getx}.dart — thin ValueListenable→Cubit/Provider stubs (~15 lines) over HintController.state, core stays dart:ui+widgets.

0.3.0 — control, accessibility, versions, visual depth #

New capabilities

  • Programmatic control: previous()/goTo(), HintTour.disableBackButton (Android back/route pop), Shift+Tab backwards navigation.
  • Smart positioning (full): auto-flip re-picks the side on scroll, keep-in-safe-area (notch/home indicator), the tooltip never covers the spotlighted targets, tail (arrow) ties the tooltip to its target.
  • Accessibility on by default: screen-reader step announcements, keyboard navigation (Tab/Shift+Tab/Enter, Esc = skip), reduce-motion (hintTransitionDuration), fits at 2× text scale, WCAG AA contrast in the default themes, focus restored after the tour.
  • Versioned hints: HintStore (shouldShow(key, minVersion:) / markShown), the "show again" semantic is a version bump, not flag wiping; InMemoryHintStore ships in the core, persistent implementations live app-side.
  • Multi-target steps: several elements spotlighted at once, each with its own scrim hole. Multi-content: several tooltips around one target that never overlap each other or the targets.
  • Tap regions: tap-on-target vs tap-on-overlay with per-step callbacks and tap position; scroll-through — the page scrolls under an active tour.
  • Blur scrim and a pulsing ring as theme options (HintTheme.imageFilter / showPulse); the default stays the cheap plain dim. The pulse renders above the blur (its own global layer).
  • Enum-typed tours: HintTour.fromEnum — the exhaustive stepFor switch makes adding/removing a step a compile error.
  • "Want a tour?" pre-dialog: showHintTourOffer with an "Apply to all pages" checkbox; declines persist per page or globally in the store.
  • UX polish: Skip is hidden on the last step of a tour (a lone Skip was already meaningless in 0.2.0); the scrim no longer flashes a wrong full-screen dim on the first frame of a step.
  • Example app reworked into a demo playground (main/home_screen/ demo_tours): visual demo card with blur/pulse style switcher and one button per feature, 10 smoke tests.

Adapters

  • hintful_bloc 0.1.1: HintCubit — a thin Cubit over HintController (no logic of its own; apps wanting an event layer write their own Bloc<AppEvent, HintState> on top).

0.2.0 — unified "hint" naming #

Breaking: the public API is renamed to a single Hint family — ShowcaseController → HintController, ShowcaseTarget → HintTarget, ShowcaseTheme → HintTheme, TourState → HintState (and Idle/ Waiting/Active), TourSpec/StepSpec → HintTour/HintStep, TargetRegistry → HintTargetRegistry, TourOverlayHost → HintOverlayHost. Files showcase_target.dart/showcase_theme.dart → hint_target.dart/ hint_theme.dart. TooltipPosition, DefaultTooltip and diagnostics (HintSkipReason, HintDiagnosticsHandler) keep their names — tooltip is the accepted term for the visual element, diagnostics were already Hint. No behavior changes.

0.1.0 — stage 0 (early engine) #

  • Registry-based targets (ShowcaseTarget(id:)) — no GlobalKey; duplicate-id policy "last wins", self-cancellation by identity in dispose.
  • Clean state machine (TourState), data-driven tests (table + fuzz).
  • ShowcaseController: start/next/skip/finish, showHint for single tips, DX3 validation with closest-id candidates for typos; headless-capable (no overlay host required).
  • Overlay engine on CompositedTransform: scrim hole follows the target via the compositor (zero scroll math), tooltip in a global layer with auto-flip placement (stage 0: sides + keep-in-screen).
  • Wait-for-target with timeout, deferred/lazy targets.
  • ShowcaseTheme ThemeExtension, light/dark from ColorScheme.
  • DX1 diagnostics: every failed show reports a reason (timeout, userSkipped, unknownTarget, ...) to a HintDiagnosticsHandler.
  • TourOverlayEngine (hidden mechanics) + defaultOverlayHost() factory; keyboard: Tab/Enter = next, Esc = skip.
  • Example app (example/): 4-step tour with deferred target, light/dark, showHint; smoke tests, tour flow tests, integration benchmark skeleton.
1
likes
160
points
453
downloads

Documentation

API reference

Publisher

verified publisherfellmonkey.ru

Weekly Downloads

Onboarding tours and tooltips for Flutter. Registry-based targets, theme-aware coach marks, smart positioning, accessible by default — no overlay until a tour starts.

Repository (GitHub)
View/report issues

Topics

#flutter #onboarding #tooltip #coach-marks #walkthrough

License

MIT (license)

Dependencies

flutter

More

Packages that depend on hintful