harbor 1.0.0-dev.1
harbor: ^1.0.0-dev.1 copied to clipboard
Keeps widgets clear of whatever covers each screen edge: system bars, the keyboard, and your own headers, tab bars and sheets. No inset arithmetic.
1.0.0-dev.1 #
A pre-release of 1.0, for consumers to retest before 1.0.0. Breaking: every name deprecated in 0.x is removed, as doc/design/DECISIONS.md planned ("removed together in one breaking release before 1.0"). Nothing else changes: the showcase videos render frame for frame as they do on 0.10.3, and a consumer written on 0.10.3's non-deprecated API compiles and passes unchanged.
- Migrating: upgrade to 0.10.3 first and run
dart fix --applythere; code with no deprecated harbor names needs no change. On 0.10.3 every fix applies, with the notes each release gave (add the import to a file that reaches harbor through a helper, #150; widen ashowimport, #146). One change reaches non-deprecated code:HarborFloat(placement:)andHarborPopover(placement:)are no longer nullable, since null only stood for "none given" beside the removed arguments. A call that passesplacement: nullleaves it out instead, for the same default: bottom centre for a float, above its child for a popover. - Removed: the 0.8 names (renamed in 0.9.0; the README's "Upgrading from 0.x" appendix and
doc/design/naming.mdmap them):- Typedefs:
HarborSea,Harbor,HarborDockKind,HarborDockState,HarborCoastStance,HarborSliverDock,HarborPontoonHandle,HarborMakeWay,HarborCoastFeature,HarborTide,HarborTidePhase,HarborTideStance,HarborDryDock,HarborTideSource,HarborWaters,HarborWatersData,HarborWatersAspect,HarborCastOff,HarborMoored,HarborFairwaySliver,HarborWakeKind,HarborWakePainter,HarborFlares,HarborFlareSlot,HarborFlareTarget,HarborFlareEntry,HarborFlareClosedReason,HarborFlarePhase,HarborFlareQueue,HarborFlareHold,HarborFlareTransitionBuilder,HarborBreakwater,HarborBeacon,HarborLighthouse,HarborLighthouseRegion,HarborChart,HarborChartOverlay,HarborFleet,HarborFleetObserver,HarborScaleModel. - Subclasses:
HarborDock(.pier,.quay,.reserve),HarborBuoy(and.anchored),HarborPortalBuoy,HarborCoast(.fixed,.titleSafe),HarborWake(.fade,.hairline),HarborWakeMask,HarborFairway(and.box),HarborTideState,HarborWakeBand,HarborPontoon,HarborDockRecord,HarborChartEntry. - Enum values:
HarborBarKind.pier/.quay,HarborKeyboardStance.float/.pilings/.dryDock,HarborKeyboardPhase.low/.rising/.high/.falling,HarborRoomAspect.coast/.docks,HarborRest.wakeEnd/.dockEdge,HarborToastTarget.sea,HarborToastClosedReason.lower. - Parameters and their getters: a bar's
wake,tide,coastandeffectiveCoast(andcopyWith(tide:));HarborAttachedBar.dockandHarborAttachedBarHandle.dock;HarborClear(bars:, tide:, mooringLine:, extra:)andHarborClear.clearanceOfthe same;HarborScrollView(mooringLine:, bars:, tide:, wake:, startsInOpenWater:),.boxandpaddingOfthe same;HarborSliverClear(bars:, tide:);HarborHandled(tide:, docks:);HarborRoom.clearanceOf(tide:);HarborFrame(buoys:, coast:, newPort:, bodyClearsTide:, wakePainter:);HarborRoot(coast:)andHarborScaledScreen(coast:);HarborSystemEdges.fixed(calmTide:);HarborKeyboardSpace(showsChildAtHighTide:);HarborRoomData(coast:, coastSteady:, docks:, docksResting:, wakes:, hasDocks:, wakesPainted:)andcopyWiththe same;HarborFadeBand.dockEdge,.wakeEnd,.dockOpacity;HarborEdgeEffect.fade(dockOpacity:);HarborFadeMask.wakes,.dockOpacity;HarborPopover.buoyBuilder;HarborSheet(headerWake:, footerWake:, footerTide:, clearsTopCoast:, bodyClearsTide:)(on.draggabletoo);showHarborSheet(breakwater:, keepsTopCoast:);showHarborDialog(inheritClearWater:)andHarborDialogRoute(inheritClearWater:);HarborLayoutRecord,HarborBarRecordandHarborInspectorEntry'sclearWater,docksandtide. - Members:
HarborKeyboard.isInOf;HarborKeyboardState.highWater,.highWaterIsEstimate,.isIn;HarborRoomData.wakeOf,.castOff;HarborRoom.steadyCoastOf,.dockFaceOf;HarborFadeMask.alphaWake;HarborToasts.raise;HarborToastEntry.lower,.raisedIn,.reachesCoast;HarborFrameRegistry.harbors,.sea;HarborController.clearWater,.clearWaterInGlobal,.fleet,.isPort,.makeWay,.addPontoon,.updatePontoon,.removePontoon,.addBreakwater,.addBreakwaterEdge, and the 0.8 names of its internals (dockedEdges,withDockOn,pontoonsOn,breakwaterCoverage,breakwaterChanges,breakwaterTopsInGlobal,flares,signals);HarborPopover.sideOfandmaybeSideOfthroughHarborPortalBuoy. - The inspector's 0.8 wire format:
ext.harbor.chart, and theclearWater,tideanddockskeys in each entry.ext.harbor.inspectorserves the same entries withroom,keyboardandbars.
- Typedefs:
- Removed: the 0.3 signal names
HarborSignals,HarborSignalSlot,HarborSignalTarget,HarborSignalEntry,HarborSignalTransitionBuilder,HarborSignalClosedReason, andcloseHarborSheet(HarborSheet.close). - Removed: the 0.7 placement spellings:
HarborBuoySide,HarborBuoyCrossAlignment,HarborAnchorSide.before/.after;HarborFloat(alignment:, margin:)andHarborFloat.anchored;HarborPopover(anchor:, side:, gap:, overlap:, crossAlignment:, crossOffset:, margin:, flips:);HarborToasts.show(alignment:, anchor:, side:, gap:, crossAlignment:, margin:); and the gettersHarborFloat.alignment,.margin,.anchor,.side,.gap,.overlap,.crossAlignment,.crossOffset, the same onHarborPopover(and.flips), andHarborToastEntry.alignment,.gap,.crossAlignment,.margin. Readplacement. - Removed: the 0.8
clear:spellings' last trace,HarborMooringLine(HarborClear.rowInsets), andHarborOpenWaterwithHarborWatersWidgetBuilder(aBuilderandHarborRoom.of(context)). - Removed: the 0.10 deprecations:
- Toast timing:
HarborToasts.show(duration:, persist:, countsEntrance:, countsWhileCovered:)(timing:),HarborToastEntry.hide(reason:)(hide()ordismiss()), and the entry's how-it-was-shown getters:builder,queue,hinge,liveRegion,animationStyle,transitionBuilder,reachesSystemEdges,countsEntrance,countsWhileCovered,persist,timing,transitionDuration,reverseTransitionDuration,lingers,shownFrom,avoidInGlobal,owner. - Barriers:
HarborSheetBarrier;showHarborSheet(isDismissible:, barrierColor:, barrierLabel:, barrierOnTapHint:);showHarborDialog(barrierColor:, barrierDismissible:, barrierLabel:)and the same onHarborDialogRoute;HarborFloat(modal:, barrierColor:, barrierLabel:)and the gettersbarrierColorandbarrierLabel(modalstays, as a getter).barrier:is the one spelling, so a sheet givenHarborBarrier.nonecan no longer be given barrier arguments it ignores, and a float or a dialog can no longer be given two spellings that disagree: those were the asserts and the silently ignored arguments, and they go with the arguments. - Sheet bars:
HarborSheet(headerEdgeEffect:, footerEdgeEffect:, footerMinimum:, footerKeyboard:)and their getters (headerBar:andfooterBar:, fromHarborSheet.defaultHeaderBaranddefaultFooterBar). - The audit's steps:
HarborYield,mode:onHarborHideBarandHarborController.hideBar, andHarborClaim.mode/HarborHideBar.mode(state:);withdrawsAtHighTide:onHarborBar.overlay,.solidand.reserve, and its getter (keyboard: HarborKeyboardStance.withdraws);duration:andcurve:on the three bar constructors andHarborKeepVisibleRegion, and their getters (animationStyle:);HarborSizing.hugBody,values,nameandindex(HarborSizing.hug(),sizing.hugs), andHarborFrame(maxExtentFraction:)with its getter (HarborSizing.hug(maxFraction:));HarborKeepInSightandHarborKeepInSightRegion(HarborCoverageListener,HarborKeepVisible,HarborLift,HarborKeepVisibleRegion);HarborPageCover.coverage,topInGlobalandmeasuresTop;HarborController.lastLayoutandrenderBox(HarborInspector.nearest). - Defaults:
HarborRoot(hinge:),HarborFrame(hinge:, barHinge:, dockHinge:)and the getters (defaults: HarborDefaults(hinge:, barHinge:)), andHarborFlarePolicywithqueueOf(HarborDefaultsScope(defaults: HarborDefaults(toastQueue:))).
- Toast timing:
- Removed from the library:
package:harbor/testing.dart, empty since trials moved toharbor_test, andHarborDockSlotfrom the exports (it is how a frame builds a bar; read bars withHarborInspector.nearest(context).bars, orbarsAroundinharbor_test). - Behaviour: none. Every default resolves as on 0.10.3; the 0.10.3 test suites pass once the tests of removed names are taken out, and no kept test's expected value changed except debug strings that listed a removed property. Debug output loses the removed properties, which printed only when given (
duration,curve,withdrawsAtHighTideon a bar;maxExtentFraction,hingeandbarHingeon a frame;hingeon a root; a sheet's header and footer knobs). A popover given a placement equal to its default,HarborAnchored(null, side: HarborAnchorSide.above), no longer prints it. - Docs:
HarborDefaultsandHarborDefaultsScope, and the README's Defaults section, say that defaults are read from below the scope that sets them: a context above it, including the context of the widget that builds the scope, sees the outer defaults (#152).
dart fix against 1.0. A data-driven fix is offered from a diagnostic, and against 1.0 an old name is undefined_* rather than deprecated. Measured in a consumer with 0.8-era code (DECISIONS.md, "Fix data after 1.0"), harbor's fix data still migrates, straight from 0.x to 1.0: type renames, member renames through a receiver, parameter renames, and the composite rewrites (placements, barriers, toast timing, sheet header and footer bars, clear:, the audit's steps, defaults:), in calls written without const. HarborDock.pier, .quay and .reserve are now keyed on HarborBar, where the class rename lands them, so HarborDock.pier(wake: e, ...) becomes HarborBar.overlay(edgeEffect: e, ...) against 1.0 too. The HarborFlarePolicy fix is guarded by its queue: argument: unguarded, against 1.0 it matched HarborFlarePolicy.queueOf(context) and crashed the analysis server, which left dart fix hanging for the whole project. Transforms that can no longer fire are removed; test_fixes/ is the golden for every one kept, and test_fixes/jump_to_1_0.dart for what is left.
What dart fix cannot do when jumping straight to 1.0. For full coverage, step through 0.10.3 and run dart fix there. Run against 1.0 directly, it leaves:
- A
constcreation of a removed class:const HarborDock.pier(...),const HarborMoored(...),const HarborSea(...). The analyzer offers no data-driven fix there (creation_with_non_type, or an undefined name). Removing theconstfirst lets the fix run, and whereprefer_const_constructorsis on,dart fixputs it back. - A renamed enum value:
HarborDockKind.pierbecomesHarborBarKind.pier, still undefined (undefined_enum_constant). WriteHarborBarKind.overlay, and the same for the others in the list above. HarborKeepInSight.revealand.coverageOf: writeHarborKeepVisible.revealand.coverageOf.- A getter read through implicit
thisinside an extension on a harbor type (extension on HarborBar { ... wake ... }): rename it by hand. Read through a receiver, or in a subclass, it migrates; so does a method called that way. HarborToastEntry.raisedInandreachesCoast, removed with what they were renamed to: no replacement.- A tear-off of a removed constructor (
HarborDock.pieras a value): write the new one. - A removed name in an import's
showlist: it stays, and the analyzer reportsundefined_shown_name. Widen the import, as #146 advised. HarborSeaTrials(tester).pumpSeaTrial(...), through the extension by name: it becomesHarborTrials(tester).pumpSeaTrial(...); writetester.pumpHarborTrial(...).
0.10.3 #
- Migrating, from any earlier release: a file that reaches harbor through a helper, without importing harbor itself, needs
import 'package:harbor/harbor.dart';added beforedart fix(#150). The analyzer offers harbor's fixes only in a file that importspackage:harbor/harbor.dart: one that callssession.frame.coaston a helper's typed field, or on a value a helper returns, or that imports a library of yours re-exporting harbor, is left as it was, still deprecated, and a second run says "Nothing to fix!". With the import added,dart fix --applymigrates it, and removes the import again when the file names nothing from it. harbor_test is the same, withimport 'package:harbor_test/harbor_test.dart';. Every deprecation message now says so: "dart fixapplies it in a file that imports package:harbor/harbor.dart". harbor cannot fix it from its side: the analyzer (Dart 3.13.3) matches a fix'suris:against the file's own imports only, not the library that declares the member (dart-lang/sdk#59764, #62491).test_fixes/helper_reach.dartrecords the result, so it fails, and this note can go, when the analyzer follows the member.
0.10.2 #
- Migrating, from any earlier release: a file that imports harbor with
showneeds the new names added afterdart fix, or the import widened (#146).dart fix --applyrenames a renamed name in theshowlist too, but a fix that brings in a name the call did not have writes it without touching the list:HarborToastTiming(timing:),HarborAligned,HarborAnchoredandHarborAnchorSide(placement:),HarborBarrier(barrier:),HarborBar,HarborEdgeEffectandHarborKeyboardStance(a sheet'sheaderBar:andfooterBar:, and a bar'skeyboard:),HarborDefaultsandHarborDefaultsScope(defaults:, andHarborFlarePolicy),HarborBarState(state:),HarborSizing(sizing:) andHarborKeepVisible,HarborLiftandHarborCoverageListener(HarborKeepInSight). The fixed file then does not compile (undefined_function,undefined_identifier), and a second run does not repair it. Widening the import toimport 'package:harbor/harbor.dart';before runningdart fixavoids it. harbor cannot fix it from its side: its fix data already names the library each of those names comes from, and the analyzer (Dart 3.13.3) writes the name through the existing import of that library without reading itsshowlist.test_fixes/show_import.dartrecords the result, so it fails, and this note can go, when the analyzer extends the list. - Internal: source files, private names and test descriptions renamed after the 0.9 vocabulary (
lib/src/buoy.dartis nowfloat.dartand its parts,dock.dartisbar.dart,tide.dartiskeyboard.dart, and so on). No public name, behaviour or import changes:package:harbor/harbor.dartexports the same names. Code that imported apackage:harbor/src/...file by path, which was never supported, needs the new path.
0.10.1 #
- A frame in an
OverlayPortal's child reads the overlay's system edges (#142). Flutter hands an overlay child the padding of the overlay it paints in, but a frame there split that padding by the room of the frame where the portal was declared: under a tab host whose bar holds the home indicator, aHarborFrame.pageshown on the root overlay read no bottom system edge, and its body ran into the home indicator. The root now publishes the padding it hands its body, and a frame whose padding on an edge is exactly the root's, where neither the outer room's system edge nor its bars reach that far, takes it all as system edge. Padding an app raises with its ownMediaQueryis split as before, and frames outside an overlay are unchanged. The recipe, a full-screen page over everything, is in the README.
0.10.0 #
-
Behaviour change:
showHarborDialogandHarborDialogRoutekeep clear of the system edges by default, asshowDialogdoes withuseSafeArea: true. A tall MaterialDialogstopped short of the keyboard but ran under the status bar and the home indicator: on an iPhone 17 its top sat at 24, under a 62 status bar. The dialog is now inside aHarborClearthat clears the system edges (not the keyboard, which a MaterialDialogalready keeps off) and marks them handled, so aHarborClearinside it does not clear them a second time.clearsSystemEdges: false, on both, leaves the dialog as it was. Measured intest/dialog_system_edges_test.dart, on an iPhone 17 and an iPhone SE. -
Migrating: a dialog that already kept clear of the system edges with a
HarborClearor aSafeAreais where it was: they now read zero there. What moves:- A Material
DialogorAlertDialog, or anything that pads by its own inset alone, moves in by the system edges: a tall one's top from 24 to 86 on an iPhone 17 (62 + 24) and to 44 on an iPhone SE (20 + 24), its bottom up by 34 on an iPhone 17. A short one is centred between the system edges, so it moves down by half their difference: 14 on an iPhone 17, 10 on an iPhone SE. With the keyboard up the bottom is where it was, since the keyboard covers the home indicator. - With
inheritRoom: true, content that did not keep clear ofMediaQuery.paddingnow keeps between the opener's header and composer, asinheritRoompromised: a full-screen child of an iPhone 17 page with a 50 header and a 60 footer runs from 112 to 780, where it ran from 0 to 874. - Content that reads
MediaQuery.paddingorviewPaddinginside the dialog reads zero for the system edges, as undershowDialog. One that lays out against the whole screen (a menu placed at an anchor's global position, a full-bleed backdrop) passesclearsSystemEdges: false, and is handed the system edges inMediaQuery.paddingas before.
- A Material
-
HarborToastTiming: how long a toast stays, as one value given toHarborToasts.show(timing:): itsduration(4 s, aSnackBar's, by default; null stays until it is hidden),countsEntranceandcountsWhileCovered.HarborToastTiming.untilHiddenis the toast with a button, an Undo. It is equal by value and describes itself in diagnostics. -
Deprecated, until 1.0:
duration:,persist:,countsEntrance:andcountsWhileCovered:onHarborToasts.show(and on the 0.8raise). They still work, with the defaults they had, and are merged into a timing; giving one withtiming:asserts.persist: trueandduration: nullsaid the same thing two ways; there is now one, a null duration. -
HarborToastEntrysays what a caller may do with a toast, and no more:hide(),dismiss()(below),closed,phase,wasShown,hold(),held,duration,showing,placement,anchor,side, andmaybeOf.anchorandsidestay for a transition that clips at its anchor, andsidestays non-null, soHarborAnchorClip(side: entry.side)still compiles. The members that are how the toast was shown, or the record harbor runs it on, are deprecated: 17 newly (builder,queue,hinge,liveRegion,animationStyle,transitionBuilder,reachesSystemEdges,countsEntrance,countsWhileCovered,persist,timing(an internal counter, not aHarborToastTiming, despite the name),transitionDuration,reverseTransitionDuration,lingers,shownFrom,avoidInGlobal,owner), beside the six already deprecated (alignment,gap,crossAlignment,margin,raisedIn,reachesCoast). They still read what they did;persistnow readsduration == null. -
HarborToastEntry.dismiss(): hides the toast because the user dismissed it, soclosedreportsHarborToastClosedReason.dismiss, as it does when a screen reader dismisses one. For a toast with its own close button, or one in aDismissible, so analytics can tell it from a toast the app hid withhide(). -
Deprecated:
hide(reason:).hide()reportsHarborToastClosedReason.hideanddismiss()reportsdismiss; the other reasons (timeout,remove,replaced) are what harbor reports for a toast it hid itself, and a caller passing one madeclosedreport something that did not happen. Passing one still works until 1.0. -
Behaviour change: a toast shown with
persist: truenow has a nullduration, where it read 4 s, and settingentry.durationon it starts its time, where before nothing ran whilepersistwas set. Measured intest/toast_timing_test.dart:persist: truewithduration: 1 sis still up after 10 s, and settingentry.duration = 1 sthen hides it 1.1 s later. A toast shown withoutpersistis unchanged. -
Breaking, for a test fake only:
HarborToastEntryis anabstract final class, soimplements HarborToastEntryno longer compiles. Its constructor was already@internal; show a toast and read the entry it returns instead. -
Migrating:
dart fix --apply, thendart formaton what it changed. It rewrites every combination of the four deprecated arguments intotiming:, through the 0.8HarborFlares.raiseand 0.3HarborSignals.raisenames too:persist: true(whateverduration:says) andduration: nullbecomeHarborToastTiming.untilHidden, orHarborToastTiming(duration: null, ...)beside a count flag;duration: dbecomesHarborToastTiming(duration: d);persist: falseis removed. What it cannot do, by hand:persist:given anything but the literaltrueorfalse(a variable): writetiming: HarborToastTiming(duration: p ? null : d), withdtheduration:it was given (HarborToastTiming.defaultDurationif none), and itscountsEntrance:andcountsWhileCovered:beside it, as a wrapper that passes all of them through needs. With no other old argument,timing: p ? HarborToastTiming.untilHidden : const HarborToastTiming()says the same. The whole call is left as it was, so its other old arguments are yours too.- Code that sets or reads
entry.durationon toasts shown withpersist: true(a helper that lengthens every toast's time for a screen reader,t.duration! * 2): guard onduration != null. Those toasts'durationis now null, which throws there, and setting one now starts a time that runs out (above). - A call that already gives
timing:beside an old argument asserts, and is left alone. - Reads of the deprecated entry members above: there is no replacement, except
placementforalignment,gap,crossAlignmentandmargin, andduration == nullforpersist. hide(reason: ...): leave the reason out, or calldismiss()forHarborToastClosedReason.dismiss.- A fake that implements
HarborToastEntry.
-
HarborBarrier: one value for what a sheet, a dialog or a float puts over the page (#120,doc/design/sheets.md).HarborBarrier.dimmed(color:, dismissible:, label:, onTapHint:)tints the page (HarborBarrier.defaultColor,showGeneralDialog's half-black, by default),HarborBarrier.clear(dismissible:, label:, onTapHint:)leaves it in view but out of reach, andHarborBarrier.noneputs up nothing.showHarborSheet,showHarborDialog,HarborDialogRouteandHarborFloatall take it asbarrier:. It replaces five sheet parameters that disagreed with each other (HarborSheetBarrier.dismissiblemeant dimmed,isDismissibleapplied to a clear barrier too,barrierColoronly to a dimmed one), three on a dialog, and three on a float, each spelled differently. Equal by value, and it describes itself in diagnostics (HarborBarrier.clear(label: "Close menu")). Read whether a tap dismisses withisDismissible: the deprecated enum below implementsHarborBarrier, and its valuedismissibleholds that name. -
A dialog's barrier says what tapping it does (
onTapHint), as a sheet's already did; so does a float's. A float's barrier can be not dismissible: a tap on it does nothing, and back and Escape still callonDismiss. A dialog cannot takeHarborBarrier.none(an assert): it is always a route with a barrier. -
HarborSheet(headerBar:, footerBar:)take the header and footer as whole bars, so every bar option is open to them: a footer that withdraws while the keyboard is up (HarborBar.solid(withdrawsAtHighTide: true, ...)), a header with no fade, a backdrop.header:andfooter:stay, and put a widget in the sheet's own bars, now published asHarborSheet.defaultHeaderBar(child)(an overlay bar with a 12-long fade) andHarborSheet.defaultFooterBar(child)(a solid bar that rides the keyboard, with a hairline and a minimum of 16), to start a bar of your own from:footerBar: HarborSheet.defaultFooterBar(actions).copyWith(keyboard: HarborKeyboardStance.stays). OnHarborSheet.draggableany header bar is still the handle that drags the sheet. A header and aheaderBar, or a footer and afooterBar, is an assert. -
showHarborSheet's documentation says howreachesTopandfillsScreenAtHingemeet: filling a screen at a hinge reaches the top, so on a split screen it impliesreachesTop, and elsewherereachesTopalone decides. They stay two switches (both can be set). The opener decides how high the sheet reaches;HarborSheet(clearsTopSystemEdge:)decides whether its content pads for the status bar. -
Deprecated, until 1.0, each still working as it did:
HarborSheetBarrier;showHarborSheet(isDismissible:, barrierColor:, barrierLabel:, barrierOnTapHint:);showHarborDialog(barrierColor:, barrierDismissible:, barrierLabel:)and the same onHarborDialogRoute;HarborFloat(modal:, barrierColor:, barrierLabel:)and the gettersbarrierColorandbarrierLabel(modalstays, as a getter: whether the float has a barrier);HarborSheet(headerEdgeEffect:, footerEdgeEffect:, footerMinimum:, footerKeyboard:)and their getters, which 0.9.0 had just renamed fromheaderWake,footerWakeandfooterTide; andHarborPageCover.coverage,topInGlobalandmeasuresTop(a page cover's only job outside harbor isremove()). A loose barrier argument given besidebarrier:overrides that field of it, andbarrierColorstill only tints a dimmed barrier.HarborDialogRoute(barrierColor: null)is still a barrier with no colour, and withHarborBarrier.nonea sheet's loose barrier arguments are still ignored, as they always were: they can return as an assert only once they are removed at 1.0. -
Behaviour: new asserts only where a call mixes the new spelling with the old: a float given
barrier:withmodal:,barrierColor:orbarrierLabel:, and a sheet given a header or footer knob with aheaderBarorfooterBar. Every call written against 0.9.1 runs as it did. Nothing moves on screen: every sheet, dialog and float test in the package and the example passes unchanged, and a plain header and footer sit exactly wheredefaultHeaderBaranddefaultFooterBardo (test/sheet_bars_test.dart). -
Behaviour: debug output. A float lists
barrier: HarborBarrier.clear(label: "Close menu")where it listedbarrierLabel: "Close menu"(andbarrierColor), and a sheet lists itsheaderBarandfooterBar. A test that compares those strings needs the new ones. -
Migrating:
dart fix --apply, thendart formaton what it changed. It rewrites ashowHarborSheetcall that gives any of its four loose barrier arguments into onebarrier:(with the literalHarborSheetBarrier.dismissible,.clearor.nonebeside them, or none), ashowHarborDialogcall that gives any of its three, aHarborDialogRoutethat givesbarrierDismissibleorbarrierLabel(andbarrierColor: null, which becomesHarborBarrier.clear(...)), aHarborFloatorHarborBuoywithmodal: true(a clear barrier, or a dimmed one with itsbarrierColor) ormodal: false, and aHarborSheetorHarborSheet.draggablewhose header or footer has a knob:header: h, headerEdgeEffect: ebecomesheaderBar: HarborBar.overlay(edgeEffect: e, debugLabel: 'sheet header', child: h), and a footer with any of its three knobs becomesfooterBar: HarborBar.solid(edgeEffect:, minimum:, keyboard:, debugLabel: 'sheet footer', child: f), the knobs it left out written with the sheet's defaults (a hairline, 16.0,rides). It drops an argument that did nothing, such asbarrierColorwith a clear barrier or a footer knob with no footer, so a migrated call does what it did. What it cannot do, by hand:barrier: HarborSheetBarrier.noneon its own: writebarrier: HarborBarrier.none. Likewise.dismissibleisconst HarborBarrier.dimmed()and.clearisconst HarborBarrier.clear(). A fix that rewrote the value everywhere would break a variable typedHarborSheetBarrier, and turn a comparison with it into one that is never true. The old values still compile where aHarborBarrieris taken, because the enum implements it.HarborSheetBarrierused as a type, in aswitch, or through.valuesand.name: useHarborBarrierand a list of your own, as the field guide's barrier picker does.- A barrier given as a variable alongside a loose argument (
barrier: b, isDismissible: false): build the barrier you mean,HarborBarrier.dimmed(dismissible: false). modal:given a variable:barrier: open ? const HarborBarrier.clear() : HarborBarrier.none. Reads offloat.barrierColorandfloat.barrierLabelarefloat.barrier.colorandfloat.barrier.label(null meaning 'Dismiss').HarborDialogRoute(barrierColor: c)for a colour other than the literalnull:barrier: HarborBarrier.dimmed(color: c), orHarborBarrier.clear()wherecis null. It takes aColor?, and a fix compares text, not types.- A header or footer that can be null, with a knob (
footer: done ? actions : null, footerMinimum: 0).dart fixputs it in the bar'schild:, which cannot be null, so the call no longer compiles (argument_type_not_assignable, on that line). Make the bar conditional instead:footerBar: done ? const HarborBar.solid(edgeEffect: HarborEdgeEffect.hairline(), minimum: 0, keyboard: HarborKeyboardStance.rides, debugLabel: 'sheet footer', child: actions) : null. A fix cannot see an argument's type. This is the one place it can break code that compiled, and it was kept because it is right everywhere else. - A read of
HarborPageCover.coverage,topInGlobalormeasuresTop: keep the cover toremove()it, and read where a frame's room ends fromHarborFrame.of(context).room. - A line under
// ignore: deprecated_member_use, whichdart fixdoes not see. The audit's smaller steps (#120), in the 0.9 names. Each old spelling still works, deprecated, with the default it had, and goes at 1.0;dart fix --applyrewrites it where the change can be read from the call (lib/fix_data/audit.yaml).
-
HarborYieldmerges intoHarborBarState: a bar hidesdarkorwithdrawn, the two states it hides into.HarborHideBar(state:)andHarborFrame.of(context).hideBar(edge, state:)take aHarborBarState,withdrawnby default asmode:was, andHarborClaim.statereads it back.HarborBarState.openis an assert there: releasing the claim is what opens the bar. Deprecated:HarborYield,mode:on both, andHarborClaim.mode/HarborHideBar.mode. Nothing moves. -
withdrawsAtHighTide: trueis a keyboard stance,HarborKeyboardStance.withdraws: the bar withdraws while the keyboard is up and comes back once it is down, laid out as a bar that stays (the keyboard covers it as it slides out) and taking the system edge as one does.reservesand withdrawing are one stance each, so a bar can no longer be asked to hold the keyboard's space and leave for it. Deprecated:withdrawsAtHighTide:onHarborBar.overlay,.solidand.reserve(and theHarborDockconstructors), and the getter, which reads true for a bar thatwithdrawstoo. The flag still works on a bar of any stance. The inspector's JSON readskeyboard: withdraws, andtide: pilingsunder the 0.8 keys. Nothing moves. -
Breaking:
HarborKeyboardStancehas a new value,withdraws, so an exhaustiveswitchover it needs a case for it. Migrating: addcase HarborKeyboardStance.withdraws:(it behaves asstays), or a default. -
duration:andcurve:go intoanimationStyle:, which already overrode them, onHarborBar.overlay,.solidand.reserve(and theHarborDockconstructors) and onHarborKeepInSightRegion. A bar with noanimationStylestill moves over 250 ms alongCurves.easeInOutCubic, and a region over 280 ms alongCurves.easeOutCubic.AnimationStyle(duration: d)is exactly whatduration: dwas: the reverse falls back to the forward value either way. Deprecated:duration:andcurve:there, and their getters.HarborKeepInSight.reveal(duration:, curve:)stays: it takes noanimationStyle. Nothing moves. -
HarborSizingis a class:HarborSizing.fill, still the default, andHarborSizing.hug({maxFraction}), which carries the cap that wasHarborFrame(maxExtentFraction:).HarborFrame(sizing: HarborSizing.hug(maxFraction: 0.5))is whatsizing: HarborSizing.hugBody, maxExtentFraction: 0.5was. Deprecated:HarborSizing.hugBody(equal tohug()),maxExtentFraction:onHarborFrameandHarborFrame.page, and its getter, which reads the sizing's fraction. The enum'svalues,nameandindexare kept, deprecated, so code that listed or named the sizings still compiles. A frame given both a fraction in its sizing and the deprecatedmaxExtentFraction:takes the sizing's. Debug output readssizing: HarborSizing.hug(maxFraction: 50.0%)where it readsizing: hugBodyand a separatemaxExtentFraction, and the unbounded-height error saysHarborSizing.hug(). Nothing moves. -
Breaking: an exhaustive
switchoverHarborSizing, aHarborSizingin anEnumProperty, or aHarborSizing.fillused as acaseof an enum switch no longer compiles: it is not an enum. Migrating: readsizing.hugs, and use aDiagnosticsProperty. -
HarborController's public surface is what content asks of a frame:hideBar,attachBar,updateAttachedBar,detachBar,addPageCover,addPageCoverEdge,room,roomInGlobal,registry,debugLabelandisPage. The rest is harbor's own plumbing and is now@internal:parent,route,isRouteLevel,withBarOn,edgesWithBars,claimedStateOf,attachedBarsOn,pageCoverage,pageCoverChanges,pageCoverTopsInGlobal,toasts,addLeaveListener,removeLeaveListener, and the staticsHarborController.ofandmaybeOf(HarborFrame.ofandmaybeOfreturn the same handle). Code outside harbor that uses them still compiles, anddart analyzereportsinvalid_use_of_internal_member. None of them is in the README. Their 0.8 names (dockedEdges,withDockOn,pontoonsOn,breakwaterCoverage,breakwaterChanges,breakwaterTopsInGlobal,flares, andsignals) stay deprecated, anddart fixno longer renames them, since the rename would trade a deprecation for that warning. -
HarborTitleSafekeeps its name: the planned fold intoHarborSystemEdgesis dropped, since the name is plain already. The docs ofHarborSystemEdgeFeature.hingeandHarborController.isPagesay what is true now: harbor keeps its overlays and bars off a hinge throughMediaQuery.displayFeatures, andisPageis true for a frame built as a page or with no frame above it, and read by nothing in harbor. -
HarborKeepInSightsplits into three widgets, one per job.HarborCoverageListener(edge:, onCoverage:, child:)reports how much of it the bars on its edge cover;HarborKeepVisible(onlyWhileFocused:, clearance:, child:)keeps itself scrolled clear of the bars and the keyboard, which is its whole job, so it needs nokeepInSight: true;HarborLift(clearance:, holdPosition:, child:)lifts over what covers the bottom inside a region, which isHarborKeepVisibleRegionnow.HarborKeepVisibleandHarborLifttakeactive:(true by default), asHarborHideBardoes, to switch the job off without rebuilding the child as another widget.HarborKeepInSight.revealandcoverageOfareHarborKeepVisible.revealandcoverageOf. Deprecated:HarborKeepInSight(forwarding exactly as before, with its old defaults), its statics, andHarborKeepInSightRegion, an alias of the new region. The 0.8HarborBeacon,HarborLighthouseandHarborLighthouseRegionchain on to them. Nothing moves. -
The page boundary is not inferred at route boundaries, as 0.9 planned: measured, it would change nothing where pages usually are and move things where they should not. A route under
HarborRootlays out the same withisPageon or off (on an iPhone 17 with a 50 overlay header and a 56 solid tab bar, the same rectangles, room data and frame size, keyboard down and up), since the root has no bars to reach in; a test pins it. Inferring it at the routes of aNavigatornested in a page's body would have moved their content 50 up, under that page's overlay header. SoHarborFrame.pagestays explicit, for a frame inside another frame that should ignore its bars, and a route needs none. The example drops itsisPage: trueon routes under the root. Nothing moves. Migrating:dart fix --apply, thendart formaton what it changed. By hand: -
A
mode:given a variable, or aHarborYieldused as a type: writestate:with aHarborBarState(HarborYield.withdrawisHarborBarState.withdrawn). The fix rewrites onlymode: HarborYield.darkandmode: HarborYield.withdrawwritten out, since a variable's type would change under it. -
duration:orcurve:beside ananimationStyle:: fold them into that style by hand (AnimationStyle(duration: d, reverseDuration: ...)). The fix rewrites a call that gives no style. -
maxExtentFraction:beside asizing:given a variable: writeHarborSizing.hug(maxFraction: f)where the variable hugs. The fix rewrites it besideHarborSizing.hugBodyand removes it besidefill, which never read it. -
A
HarborKeepInSightthat does two jobs at once (onObscured:withkeepInSight:, say): nest the widgets,HarborCoverageListener(onCoverage: f, child: HarborKeepVisible(child: c)). The fix rewrites a call that does one, with a variablekeepInSight:orlift:becomingactive:. A test that found it withfind.byType(HarborKeepInSight)finds the new widget's type after the fix. AHarborKeepInSightgiven no job at all (noonObscured,keepInSightorlift) did nothing, and has no argument for the fix to key on: remove it. -
A use of an
@internalcontroller member: where the bars are,HarborInspector.nearest(context)(bars,room) reads it in global coordinates; to hide or attach a bar, the public calls above.HarborController.of(context)isHarborFrame.of(context). -
withdrawsAtHighTide:given a variable, or on a bar thatridesorreserves: the fix leaves it, since one stance cannot say both. A bar that rides and withdraws is usually the inner bar on a riding composer, whichwithdrawsplaces the same way, since an inner bar stacks on the bar outside it.
HarborDefaults: one place for app- and page-level policy (#120, doc/design/defaults.md).
HarborDefaultsholds the choices an app or a page makes once for everything beneath it:hingeandbarHinge,barrierLabel,sheetBarrierOnTapHint,sheetMaxWidth,toastQueue,toastTiming,toastAnimationStyleandtoastMargin.HarborRoot(defaults:)sets them for the app,HarborFrame(defaults:)(andHarborFrame.page) for a page and what it opens, andHarborDefaultsScope(defaults:, child:)for any other subtree. Every field is nullable; the nearest scope that sets a field wins, field by field, so a page that setstoastQueueunder a root that setsbarrierLabelkeeps the root's label. An argument given to the call itself (showHarborSheet(maxWidth:),HarborToasts.show(queue:), a barrier's ownlabel) wins over all of them, andHarborDefaults.fallbackis harbor's own, written once. Read them withHarborDefaults.of(context)in a build method, orHarborDefaults.read(context)once from a handler. Equal by value, and it describes itself in diagnostics (HarborDefaults(barrierLabel: "Fermer", toastQueue: replace)).- A Material app sets its barrier label, its sheets' tap hint and their width once, in
MaterialApp.builder, instead of on everyshowHarborSheet,showHarborDialogand modalHarborFloat:HarborRoot(defaults: HarborDefaults(barrierLabel: l.modalBarrierDismissLabel, sheetBarrierOnTapHint: l.scrimOnTapHint(l.bottomSheetLabel), sheetMaxWidth: 640), child: child!). toastMarginapplies only to a toast whose placementHarborToasts.showbuilds itself: one at aslot:, or at none. A toast given aplacement:keeps that placement's margin, since a placement always carries one; so does one given the deprecatedalignment:, sodart fixmoving it to a placement does not move it.toastTimingapplies to a toast shown with none oftiming:,duration:,persist:,countsEntrance:orcountsWhileCovered:; the deprecated four keep the defaults they had.- A root starts from
HarborDefaults.fallback, not from what is above it: a root mounted inside a frame (a phone drawn in a page) is a world of its own, as its keyboard gauge and registry already were. - Deprecated, until 1.0, each still working as it did:
HarborRoot(hinge:),HarborFrame(hinge:, barHinge:)andHarborFrame.page(hinge:, barHinge:), with their getters, and the 0.8dockHinge:. Each is folded into the frame's defaults, anddefaults:wins where both are given. The other choice was to keep them as sugar fordefaults:; deprecating them leaves one place to set a policy and one rule for which wins, anddart fixmakes the move (doc/design/DECISIONS.md, Defaults).HarborFlarePolicy(queue:, child:)is now aStatelessWidgetthat buildsHarborDefaultsScope(defaults: HarborDefaults(toastQueue: queue)), its constructor deprecated, and the deprecatedHarborFlarePolicy.queueOf(context)readsHarborDefaults.read(context).toastQueue. The class itself carries no@Deprecated, so naming it as a type is not reported: with it,dart fix --applycrashed the analysis server onHarborFlarePolicy.queueOf(...)(Dart 3.47.4), andtest_fixes/defaults.dartkeeps that call to catch it.HarborHinge.of(context)stays, and readsHarborDefaults.of(context).hinge. - Behaviour change: a page's choices reach what it opens. The defaults travel with the themes a sheet, a dialog or a toast already captures from the page that opened it, so a sheet opened from
HarborFrame(defaults: HarborDefaults(hinge: HarborHinge.never)), or from the deprecatedHarborFrame(hinge: HarborHinge.never), keeps that policy for itself, its floats and the toasts shown from it. Before, the page's hinge policy stopped at the route, and a toast shown from such a sheet's content took the root'savoidSeparating. Measured intest/defaults_test.dart:HarborHinge.ofinside the sheet readsnever, and a toast shown there under a page'stoastQueue: replacereplaces the one up; both tests fail when the capture is taken out. - Behaviour change:
barHingeis inherited. Through 0.9.1,barHinge:(and the 0.8dockHinge:) was read by its own frame alone, so a full-screen frame nested in a page withbarHinge: HarborHinge.never(a nestedNavigator's page, a tab's own frame) kept its bars off the hinge. Now it reaches every frame beneath, ashingealways did: on the dual-screen trial device (540 | 34 hinge | 540), such a nested frame's solid tab bar now runs to 1114, across the hinge (test/defaults_test.dart, beside a control that stops at 540 with no defaults). That it stopped at 540 under 0.9.1 is read from the 0.9.1 source, where only the frame's ownbarHingereached its bars, not run. The nearest setting still wins per frame, and a frame's own setting beats anything inherited: its bars follow its ownbarHinge, then its ownhinge, then the inheritedbarHinge, then the inheritedhinge, the order 0.9.1 had. So a nested frame that sets its ownhinge(HarborFrame(defaults: HarborDefaults(hinge: HarborHinge.avoidSeparating)), or the deprecatedhinge:) keeps its bars, and the frames beneath it, off the hinge, whateverbarHingethe frame around it set: measured at 540 on the same device, throughdefaults:and through the deprecated arguments, and for the formdart fixwrites from them (test_fixes/defaults.dart).HarborDefaults.mergecarries the rule: a nearerhingereplaces an inheritedbarHinge. - Behaviour: a call that sets nothing new lays out exactly as before: every default resolves to what 0.9.1 hard-coded ('Dismiss', no tap hint, no sheet width cap,
wait, 4 s, 220 ms, 16), every existing test in the package,harbor_testand the example passes. The only test edits aredart fixmoving three hinge tests todefaults:and the field guide test reading the new code panel; the example's only code edit is the field guide's hinge page, migrated bydart fix, whose code panel now readsdefaults: HarborDefaults(hinge: ...). - Behaviour: debug output.
HarborRootandHarborFramelistdefaults: HarborDefaults(...)when given, andhinge:/barHinge:only when the deprecated arguments are. - Breaking, for code that looked it up by type:
HarborFlarePolicyis no longer anInheritedWidget, socontext.getInheritedWidgetOfExactType<HarborFlarePolicy>()finds nothing. ReadHarborDefaults.read(context).toastQueue.
Migrating: dart fix --apply, then dart format on what it changed. It rewrites HarborRoot(hinge: h) to HarborRoot(defaults: HarborDefaults(hinge: h)), every combination of hinge:, barHinge: and the 0.8 dockHinge: on HarborFrame and HarborFrame.page into one defaults: (through the 0.8 HarborSea and Harbor names too), and HarborFlarePolicy(queue: q, child: c) to HarborDefaultsScope(defaults: HarborDefaults(toastQueue: q), child: c) (lib/fix_data/defaults.yaml). What it cannot do, by hand:
-
A call that already gives
defaults:beside an old argument: move the old argument into it,defaults: HarborDefaults(toastQueue: ..., hinge: h). It still compiles and runs as it is,defaults:winning, so the fix leaves it rather than guess. -
Reads of the deprecated getters:
frame.hingeandframe.barHingeareframe.defaults?.hingeandframe.defaults?.barHinge, androot.hingeisroot.defaults?.hinge ?? HarborHinge.avoidSeparating.HarborFlarePolicy.queueOf(context)isHarborDefaults.read(context).toastQueue!. -
barrierLabel:,onTapHint:andmaxWidth:repeated on every call still work, as overrides. Lifting them to the root is a refactor, not a fix. -
A nested frame that relied on
barHinge:stopping at its own frame (above). -
A line under
// ignore: deprecated_member_use, whichdart fixdoes not see. -
Dialogs that fill the screen (with the change that keeps dialogs clear of the system edges by default, from
dialog-safe): a full-screen dialog that paints its own background behind the status bar and the home indicator, as a MaterialDialog.fullscreenor a photo viewer does, now has that background stop at the system edges, so the barrier shows in those bands instead. PassclearsSystemEdges: falsetoshowHarborDialogorHarborDialogRouteto keep it running edge to edge; its content is then handed the system edges inMediaQuery.paddingto keep its own controls clear, as before. Twodart fixdefects found migrating a real app from 0.6.3 to 0.9.1 (#143): -
Fixed: a bare
pumpSeaTrial(...)inside an extension onWidgetTester(notester.in front) was rewritten intoHarborTrials(...), an extension override that does not compile. It now becomespumpHarborTrial(...), astester.pumpSeaTrial(...)always did. The analyzer matches a call with no receiver against a rename of the extension that declares it, as well as against the method's own rename, and the one listed first wins; the method's now comes first. Every renamed harbor_test member, and harbor's renamed getters and methods, is now tested through implicitthistoo: none of the others was affected. -
Fixed:
dart fixgave an aligned toastplacement:and kept aside:,gap:orcrossAlignment:it was also given (HarborFlares.raise(alignment: a, gap: 8)), which asserts beside a placement. Without an anchor they did nothing, so the fix now removes them. -
A toast's deprecated
anchor:,alignment:,side:,gap:,crossAlignment:andmargin:say thatdart fixdoes not rewrite a call giving bothanchor:andalignment:. Which of the two applies depends on their values, which a fix cannot see. Migrating, by hand: write the placement the call meant. For the usual shape, an anchor that can be null with an alignment for when it is,anchor: a, gap: 8, alignment: a != null ? null : Alignment.topCenter, margin: EdgeInsets.zeroisplacement: a == null ? const HarborAligned(alignment: Alignment.topCenter, margin: EdgeInsets.zero) : HarborAnchored(a, gap: 8, margin: EdgeInsets.zero).anchor:alone is still rewritten, whatever it is given: toHarborAnchored(a, ...), which needs an anchor, so whereacan be null make the placement conditional the same way.
0.9.1 #
- Fixed: an
AppBarwith abottom:in atopbar threw "RenderFlex children have non-zero flex but incoming height constraints are unbounded", since a bar leaves its child's height unbounded and theAppBarlays its toolbar out in aColumnwith aFlexible. APreferredSizeWidgetin atoporbottombar is now held to its own height, asScaffoldholds itsappBar. Measured on an iPhone 17 (a 62 status bar), solid and overlay alike: anAppBarwith a 4-highbottom:is 60 high and the bar reaches 122; with aTabBarit is 104 and reaches 166; on an iPhone SE (20) the same bars reach 80 and 124. A plainAppBaris unchanged at 56 below the status bar, and one whose toolbar height comes only fromAppBarTheme.toolbarHeight(80) keeps it, with abottom:too (84, 128). The height is the child's intrinsic height, since harbor imports no Material and anAppBar'spreferredSizeleaves the theme out; where the child holds aLayoutBuilderor a sliver or two-dimensional viewport, which cannot be asked, it is the child'spreferredSize. Side bars are unchanged: a preferred size is a height.
0.9.0 #
- The rename (#120). harbor's API is named after the model it implements, not the harbour it was drawn as: every edge of the screen is claimed once, by the layer that owns it, and everything inside gets what is left. What is in the way is the system edges and the keyboard; what claims an edge is a bar; what is left is the room. So
HarborSeaisHarborRoot,HarborisHarborFrame(with aHarborFrame.pageconstructor),HarborDock.pier/.quayareHarborBar.overlay/.solid,HarborTideisHarborKeyboard,HarborCoastisHarborSystemEdges,HarborMooredisHarborClear,HarborFairwayisHarborScrollView,HarborWakeisHarborEdgeEffect,HarborBuoy,HarborPortalBuoyandHarborFlares.raiseareHarborFloat,HarborPopoverandHarborToasts.show,HarborBeaconandHarborLighthouseareHarborKeepInSight, andHarborChartisHarborInspector. Their members and parameters follow: a bar'stide:iskeyboard:,coast:issystemEdges:,wake:isedgeEffect:,clearWaterisroom, and the room's fade bands arefadeBands. The README's "Renamed in 0.9" table has the main names anddoc/design/naming.mdhas every one. TheHarborprefix stays. Every 0.8 name still works, deprecated, and goes at 1.0. - Booleans that say what something keeps clear of read
clears…, asbodyClearsKeyboardandclearsSidesalready did:tide:onHarborMoored,HarborFairway,HarborFairwaySliver,HarborCastOffandpaddingOfisclearsKeyboard:, and 0.8.0'sbars:(andHarborCastOff(docks:)) isclearsBars:, sobars:is deprecated one release after it arrived. A bar'skeyboard:stays aHarborKeyboardStance. - An old parameter and its new name can both be given, for the deprecation window; when they disagree it is an assert (
HarborClear(tide: true, clearsKeyboard: false)), where the old one used to win quietly. - Not renamed yet, on purpose:
HarborFlarePolicy(it folds intoHarborDefaultsScope),withdrawsAtHighTide(it becomes aHarborKeyboardStancevalue),HarborYield(it merges intoHarborBarState),HarborTitleSafe(it folds intoHarborSystemEdges),HarborDockSlot(leaving the exports),HarborController's internals (going@internal), andHarborSizing(becoming a class). Each keeps its 0.8 name until its own step retires it, so it is renamed once. The source file names (buoy.dart,dock.dart,tide.dart, ...) and private names are renamed mechanically before 1.0. - Breaking: the
HarborClearenum, deprecated in 0.8.0, is removed, and with itclear:onHarborMoored,HarborMoored.clearanceOf,HarborFairway,HarborFairway.box,HarborFairway.paddingOfandHarborFairwaySliver, so the nameHarborClearcan go to the widget.clearsBars: falseis whatclear: HarborClear.coastwas. - Breaking, until
dart fixruns: twelve old names are deprecated subclasses of their new classes rather than aliases.HarborDock,HarborBuoy,HarborCoast,HarborWakeandHarborFairwayare subclasses so thatdart fixcan rewrite calls of their named constructors.HarborTideState,HarborWakeBand,HarborWakeMask,HarborPortalBuoy,HarborPontoon,HarborDockRecordandHarborChartEntryare subclasses that take the old names of their parameters, so that the new constructors keep their parametersrequired. A variable typed with one of those old names that holds a value the API returns no longer compiles:final HarborTideState s = HarborTide.of(context);,final HarborWake w = dock.wake;,final List<HarborDock> top = page.top;,final HarborChartEntry? e = HarborChart.nearest(context);. So does a function whose parameter is typed with one, given where the API expects the new type: awakePainterdeclared as(context, Map<HarborEdge, HarborWakeBand> wakes, body)is not aHarborEdgeEffectPainter.dart fix --applyrenames the types, and then they compile. - Breaking:
HarborLayoutRecord, which keeps its name, takes only the new names of its parameters (room:,bars:,keyboard:).dart fixrenames the old ones in a call that gives them. HarborOpenWaterandHarborWatersWidgetBuilderare deprecated, not renamed: aBuilderthat readsHarborRoom.of(context)does what they did.HarborMooringLineis deprecated forHarborClear.rowInsets(child:).- The VM-service extension is
ext.harbor.inspector.ext.harbor.chartis still served, and each entry still carries the 0.8 keys (clearWater,tide, anddockswithkind: pier/quayand each dock'stide) beside the new ones (room,keyboard,barswithkind: overlay/solid), until 1.0. - Debug output uses the new names: a bar reads
HarborBar.overlay(keyboard: rides)where a dock readHarborDock.pier(tide: float)(aHarborDockstill readsHarborDock.pier), and labels such astide ignoredandmooring linereadkeyboard ignoredandrow insets. A test that compares those strings needs the new ones. - Parameter names that showed in hovers and generated overrides changed too:
attachBar(edge, bar),updateAttachedBar(handle, edge, bar),HarborFrameObserver.didJoin(frame)/didLeave(frame),HarborKeepInSight.coverageOf(box, frame, edge). - Fixed (
dart fix): the 0.7 placement fix turnedHarborFlares.raise(anchor: a, reachesCoast: true), which worked (reachesCoastdoes nothing by an anchor), into a call with aHarborAnchoredplacement andreachesCoast, which asserts. It now dropsreachesCoastfrom an anchored call. - Migrating:
dart fix --apply, thendart formaton what it changed: it rewrites every type, value, member, parameter and named constructor that was renamed (the list above says which were not), harbor_test's names too (with harbor_test 0.4.0), and the 0.7 and 0.8 fixes still apply on the way. So a 0.6 call toHarborFlares.raise(alignment: ...)comes out asHarborToasts.show(placement: HarborAligned(...)),HarborMoored(clear: HarborClear.coast)asHarborClear(clearsBars: false), and a 0.8HarborMoored(bars: b, clear: HarborClear.coast), which 0.8 read asb && false, asHarborClear(clearsBars: false). What it cannot do, by hand:HarborOpenWater(builder: (context, waters) => ...)becomesBuilder(builder: (context) { final HarborRoomData waters = HarborRoom.of(context); ... }).HarborMooringLineused as a type or torn off (HarborMooringLine.new): a call becomesHarborClear.rowInsets(...), which builds aHarborClear, sofind.byType(HarborMooringLine)finds none once the rows are migrated. Find a row-insets widget by aKeyyou give it, or withfind.byWidgetPredicate((w) => w is HarborClear && w.rowInsets).- An override of
HarborFrameObserver.didJoinordidLeavethat names its parameterharborkeeps working, andavoid_renaming_method_parameters(influtter_lints) now reports it: rename the parameterframe. clear:given anything butHarborClear.coastorHarborClear.everything(a variable, a conditional): writeclearsBars:with abool.- A tool reading
ext.harbor.chart's JSON: move it toext.harbor.inspectorand the new keys before 1.0. - A string compared with harbor's debug output (above).
- A line under
// ignore: deprecated_member_use, whichdart fixdoes not see. - A tear-off of
HarborFlares.raisestored with a function type that namesreachesCoast: the tear-off becomesHarborToasts.show, whose parameter isreachesSystemEdges.
- The README, the dartdoc and the field guide use the new names. Field guide entries are titled by the new class names, with the harbour drawings kept as illustrations. The videos are unchanged: they still narrate the harbour words, and are re-voiced in a later release.
0.8.0 #
bars:onHarborMoored,HarborMoored.clearanceOf,HarborFairway,HarborFairway.box,HarborFairway.paddingOfandHarborFairwaySliver: whether the edges keep clear of the bars (the docks) as well as the coast.bars: falseis exactly whatclear: HarborClear.coastwas, and true, the default, isHarborClear.everything. It places the same on every widget, including how it meetstide:, which differs between them as it did before: a moored widget withbars: falseleaves the keyboard out whatever itstide, while a fairway's bottom end still keeps clear of the keyboard unlesstideis false. This is the first step of the rename (#120): the nameHarborCleargoes to the widgetHarborMoored, so the enum has to give it up first.- Deprecated:
clear:on all six, and theHarborClearenum.clear:still works and forwards tobars:; givingclear: HarborClear.everythingwithbars: falseasserts. Thecleargetters read back whatbarssays. Debug properties showbars not clearedwhere they showedclear: coast. Migrating:dart fix --applyrewrites the two literal spellings,clear: HarborClear.coasttobars: falseandclear: HarborClear.everythingto nothing. Aclear:given a variable, and aHarborClearused as a type, are yours to change:bars: value != HarborClear.coast, or better, aboolfrom the start.
0.7.0 #
HarborPlacement: where an overlay sits in the clear water, as one value thatHarborBuoy(placement:),HarborPortalBuoy(placement:)andHarborFlares.raise(placement:)all take, so a placement written for one means the same in the others.HarborAligned(alignment:, margin:)puts it at a point of the water (bottom centre, 16 in, by default);HarborAnchored(anchor, side:, gap:, overlap:, crossAlignment:, crossOffset:, flips:, margin:)puts it by an anchor (below it, 8 away, centred, 8 in). Both are equal by value and describe themselves in diagnostics.HarborPlacement.aligned(...)andHarborPlacement.anchored(...)spell the same. A portal buoy'sHarborAnchoredmay leave the anchornullto anchor to its child; a buoy and a flare need one (asserted).flipsnull keeps each host's way: a portal buoy flips, a buoy and a flare keep their side.HarborFlares.raise(slot:)stays, as the short way to say the four heights, and is given instead of a placement, not with one (asserted).- Flares gain what buoys had: an anchored flare takes
overlapandcrossOffset, andHarborFlareEntry.placementreads back the whole placement.HarborBuoy(within:)now keeps an anchored buoy inside it too. A flare never flips, so its transition knows its side:HarborAnchored(flips: true)given toraiseis an assert. - Buoys, portal buoys and flares are placed by one engine inside harbor, so the three no longer keep their own copies of the arithmetic (the copies that drifted into the 0.6.2 and 0.6.3 fixes).
- Behaviour that moves things, on a dual-screen device: every overlay now keeps to its screen first and then takes its margin off that screen, so it keeps its margin off a hinge as it does off an edge. Anchored buoys, portal buoys and flares with no harbor already did; an aligned buoy or flare in a harbor took its margin off before splitting at the hinge, so it ran up to the hinge. Measured on the dual-screen trial device (540 | 34 hinge | 540): a 200-wide buoy at the default bottom centre is centred at 270 on the left screen, where it was at 278, and one as wide as it may be runs 16 to 524, where it ran to 540. An anchored buoy is also laid out in the largest screen of the water rather than across both, as a portal buoy already was. Migrating: nothing to change; a buoy meant to run up to the hinge takes
margin: EdgeInsets.zeroon its side, orhinge: HarborHinge.never. - Behaviour that moves things: a flare with no harbor above it that reaches the coast is handed only the coast it covers as
MediaQuery.padding, its margin taken off, as a flare in a harbor already was. Measured on the iPhone 17 on its side with a 10 margin at the low slot: its content reads 52 at each side and 11 at the bottom, where it read 62 and 21. Withmargin: EdgeInsets.zero, the bar's look, nothing changes. - Behaviour that moves things, on a dual-screen device, right to left: a flare with no harbor above it whose point is on the hinge (a centred one) goes to the screen where reading starts, the right one right to left, as a flare in a harbor already did; it went to the left one whatever the direction. Measured on the dual-screen trial device right to left: a 200-wide low flare runs 744 to 944, where it ran 170 to 370. Migrating: nothing to change;
HarborHinge.nevercentres it across both screens. - Behaviour that moves things, on a dual-screen device: a flare with no harbor above it, by an anchor near the hinge, keeps 8 off the hinge rather than 8 plus the device's padding on the far edge of the other screen, which it took off before splitting at the hinge. Measured on the dual-screen trial device with a 40 coast on the right: a 200-wide toast by an anchor ending 20 short of the hinge ends at 532, where it ended at 492. Migrating: nothing to change.
- Fixed: a flare in a harbor on a dual-screen device read the whole screen's
MediaQuery, so its content cleared the other screen's padding and saw the hinge; it now reads its own screen's, as a flare with no harbor above it already did (and does again: the engine had dropped it). Measured on the dual-screen trial device with a coast of 40 on the right and 20 at the bottom: a low flare on the left screen reads a padding of 20 at the bottom and none on the right, and no display features, where it read 40 on the right and the hinge. Withhinge: HarborHinge.neverit spans both screens and reads the wholeMediaQuery, the hinge included, where with no harbor it read no display features. reachesCoast: truewith aHarborAnchoredplacement is an assert: an anchored flare sits by its anchor, clear of the coast, and since 0.6.3 the option had no effect there. The deprecatedanchor:spelling keeps it as the no-op it was.HarborBuoySideis renamedHarborAnchorSide, andHarborBuoyCrossAlignmentHarborCrossAlignment: flares andHarborAnchorCliptake them too, so they are not a buoy's alone. The old names are deprecated typedefs.- Deprecated, until 1.0:
HarborBuoy(alignment:, margin:),HarborBuoy.anchored,HarborPortalBuoy(anchor:, side:, gap:, overlap:, crossAlignment:, crossOffset:, margin:, flips:),HarborFlares.raise(alignment:, anchor:, side:, gap:, crossAlignment:, margin:), the gettersHarborBuoy.alignment,.margin,.anchor,.side,.gap,.overlap,.crossAlignmentand.crossOffset, the same onHarborPortalBuoy(andflips), andHarborFlareEntry.alignment,.gap,.crossAlignmentand.margin(readplacement).HarborFlareEntry.anchorand.sidestay, for a transition that clips at its anchor. Each deprecated spelling keeps the defaults it had:HarborBuoy.anchoredandHarborPortalBuoysit above their anchor, where aHarborAnchoredsits below, and a portal buoy given no placement at all still sits above its child. - Migrating: run
dart fix --apply, thendart formaton what it changed: it leaves a rewritten argument on the line it found, so the layout comes out uneven. It renames the two enums and theirbeforeandaftertostartandend, renames theHarborSignal*names flares had through 0.3.0 (soHarborSignals.raise(...)is migrated too), and rewritesHarborBuoy(alignment:, margin:), every call toHarborBuoy.anchored(...),HarborPortalBuoy(...)with any of its eight old placement arguments, andHarborFlares.raise(alignment:, anchor:, ..., margin:)into a placement, writingside: HarborAnchorSide.abovewhere an old anchored buoy or portal buoy left its side out, so a migrated call sits where it did.raise(slot: s, margin: m)becomesplacement: HarborAligned(alignment: s.alignment, margin: m). What it cannot do, by hand: a tear-off ofHarborBuoy.anchored(it is left as it is, still compiling and still deprecated); a line under an// ignore: deprecated_member_use, whichdart fixdoes not see; reads of the deprecated getters above; code that relied on the moves above; and anentry.marginread for null, which now always has the margin in effect.
0.6.3 #
- Fixed:
HarborFlares.raise(reachesCoast: true)has no effect on a flare raised withanchor:in a harbor, as it already had with no harbor above it; it reaches over the coast only from a slot or an alignment at an edge. Before, the harbor laid the anchored flare out in the water reaching over the coast but placed it by its anchor in the water clear of it, and handed its content the coast that wider water covered as its padding. Measured on the iPhone 17 on its side: a toast as wide as it may be by an anchor in the middle now runs 70 to 804 with the page's padding (62 at each side), as withoutreachesCoast, where it ran 70 to 928, over the right coast, with 54 at each side.
0.6.2 #
- Fixed: a flare's hinge policy is honoured however it is placed. Two of the four paths ignored it: an anchored flare in a harbor ignored
HarborFlares.raise(hinge:), and an aligned flare with no harbor above it took the whole width of both screens whatever its policy. Measured on the dual-screen trial device: a 200-wide toast by an anchor ending 20 short of the hinge now ends at 532, 8 in from the hinge, where it was centred across it; a low-slot toast with no harbor keeps to one screen, and withhinge: HarborHinge.neveris centred across both. - Fixed: a
HarborBuoy.anchoredkeeps to the screen its anchor is on when a hinge or fold itsHarborHingekeeps off splits the screen, as aHarborPortalBuoyhas since 0.5.3; before, it ignoredhinge:and could hang across the hinge.hinge: HarborHinge.neverkeeps the old placement.
0.6.1 #
HarborSheetController.close()andremove()do nothing once the sheet has left, however it went (closed by the controller, its page popped, or torn down with its overlay since 0.6.0), andclosedis complete then, where before they asserted. A widget can close the sheet it opened from its owndisposewithout keeping a flag of its own;isAttachedstill says whether a sheet is up.animationandsetStatestill need one. (#121)HarborFlareEntry.phase(HarborFlarePhase.waiting,shown,closed) andwasShownread a flare's life synchronously, updated beforeclosedcompletes: a flare lowered while it waits isclosedby the timelower()returns, withwasShownfalse. A flare raised into a harbor and lowered in the same frame now leaves at once instead of being built once, unseen, to linger. (#122)
0.6.0 #
- Breaking:
HarborFlareClosedReasonhas a new value,replaced, so an exhaustiveswitchover it needs a case for it. Migrating: addcase HarborFlareClosedReason.replaced:(or a default) wherever you switch over a flare's reason. HarborFlares.raise(queue: HarborFlareQueue.replace)takes its place's turn at once: the flare goes to the front and the one up is lowered withHarborFlareClosedReason.replaced, leaving at once so the new one shows in its place.HarborFlarePolicy(queue:, child:)sets it for a subtree, and a raise's ownqueue:overrides it.HarborFlareQueue.wait, the default, keeps today's turn-taking. Both still completeclosedexactly once per flare. (#117)- Fixed: a sheet with no barrier torn down with its overlay while still open (a navigator's subtree replaced on sign-out, a test swapping the tree) left its transition running on the disposed overlay, which failed with an active ticker in debug and leaked it in release, and its future never completed. It is now closed at once, its transition stopped and disposed, and its future completed with null, as when its page pops. (#118)
0.5.4 #
HarborFlares.raise(countsWhileCovered: true)keeps a flare's time running while another route covers its page, so a toast confirming an action goes on schedule even if a page is pushed over it; a hold still stops it.falsestays the default. (#113)Harbor(dockHinge:)sets the hinge policy for a harbor's own docks alone:dockHinge: HarborHinge.neverlays a tab bar across a dual-screen hinge while the sheets, dialogs, flares and menus raised beneath it keep the policy above (keeping off the hinge by default).hinge:still sets both. Measured on the dual-screen trial device: withdockHinge: neverthe tab bar runs 0 to 1114 and a sheet from the page keeps to 0 to 540. (#114)
0.5.3 #
- A
HarborPortalBuoykeeps to the screen that holds its anchor when a hinge or fold itsHarborHingekeeps off splits the screen, as a Material menu keeps to its button's screen, and flips within that screen; before, a menu from a button by the hinge could hang across it.HarborPortalBuoy(hinge:)sets its own policy, the page's otherwise. Measured on the dual-screen trial device: a 200-wide menu from a button ending at 530 now ends at 532, 8 in from the hinge, where it was centred across it. An anchored flare with no harbor above it does the same. showHarborSheet(fillsScreenAtHinge: true): a sheet a hinge keeps to one screen fills that screen top to bottom, keeping the status bar as its top coast, as Microsoft suggests for a partial overlay on a dual-screen device;anchorPointpicks the screen. With no hinge it changes nothing.
0.5.2 #
HarborPosture.of(context)reads how a foldable is held from its fold:flat,tabletop(half open, the fold across the screen) orbook(half open, the fold down the middle), as Android's postures are. AHarborPanesalready splits at a half-open fold under the defaultHarborHinge, so on a table its start pane sits above the fold and its end pane below: measured on a 750 by 832 foldable half open at 416, the start pane is 0 to 416 and a quay in the end pane sits on the lower screen's bottom coast.
0.5.1 #
HarborPanes(start:, end:), two panes modelled on Windows' and .NET MAUI'sTwoPaneViewand Microsoft'sdual_screenTwoPane: either side of a hinge or fold itsHarborHingekeeps off (side by side for a vertical one, start on top for a horizontal one), side by side when the width reachesminWideWidth(840 by default), one above the other when the height reachesminTallHeight(off by default), and otherwisepriority's pane alone. Each pane gets aMediaQueryof its own: its size, the coast only on its outer edges, and only the display features inside it, so each can be aHarborwith its own docks.HarborPanes.modeOf(context)reportssingle,wideortall. Measured on the dual-screen trial device: panes at 0 to 540 and 574 to 1114, each 540 by 720 with the top and bottom coast and no hinge in it.
0.5.0 #
- Breaking (on a device with a hinge): docks keep off a hinge or fold the page's
HarborHingekeeps off, as Microsoft's dual-screen guidance and Android's foldable guidance both ask, where before a bottom tab bar or a centred title could sit under a dual-screen device's hinge. Atoporbottomdock across a vertical hinge keeps to the screen where reading starts (the left, or the right right to left), and astartorenddock down a horizontal hinge keeps to the top screen. Measured on the dual-screen trial device (540 | 34 hinge | 540): a tab bar runs 0 to 540, or 574 to 1114 right to left. The body still runs across. Only a harbor as big as the screen reads the hinge. Nothing changes on a phone, a tablet or a flat foldable. Migrating: a page meant to lay its bars across the hinge passesHarbor(hinge: HarborHinge.never).
0.4.6 #
HarborHinge, a policy for which hinges and folds harbor's overlays keep off, modelled on Material 3'sHingePolicy:avoidSeparating(the default, unchanged: a hinge with width and a half-open fold),avoidOccluding(only a hinge with width),always(every hinge and fold, a flat one too) andnever(nothing, so a foldable's inner screen is one unbroken screen). Set it onHarborSea(hinge:)for the app,Harbor(hinge:)for a page, andshowHarborSheet(hinge:),showHarborDialog(hinge:),HarborFlares.raise(hinge:)orHarborBuoy(hinge:)for one overlay; the nearest wins, and a flare takes the policy of the place that raised it.MediaQuery.displayFeaturesis left as the platform reports it, so widgets that read the hinge still see it. Measured on the dual-screen trial device (540 | 34 hinge | 540): withnevera sheet runs 0 to 1114 and a dialog is centred at 557.
0.4.5 #
- Fixed: a sheet with no barrier (
showHarborSheet(barrier: HarborSheetBarrier.none)) hid whenever a dialog, a modal sheet or a menu opened over its page, though aScaffold's persistent bottom sheet stays in view under one. In a page built from a harbor, the page now draws the sheet through anOverlayPortal, just above itself and below every route pushed after it, so a dialog sits over the sheet, which stays in view behind its barrier and out of reach. A page pushed on top still covers it, and back, Escape, focus and breakwater reporting are unchanged. Simply showing the old sheet under a dialog was not safe: the navigator keeps an overlay entry that is not a route's above all of its routes, dialogs included, so the sheet would have been drawn over the dialog and taken taps through its barrier. A page with no harbor keeps that entry, and the sheet still hides there while anything is over the page. (#104)
0.4.4 #
HarborFlares.raise(reachesCoast: true)lets a flare run over the coast on the edges it is against (under the home indicator at the bottom, the status bar at the top, the side insets always), still off the docks and the keyboard, and hands its content the coast it covers asMediaQuery.padding:SnackBarBehavior.fixed's look, with a flare's queue, time and semantics. Measured on an iPhone 17 withmargin: EdgeInsets.zeroat the low slot: the bar runs from 834 to 874 and its content reads a bottom padding of 34. (#100)HarborFlares.raise(countsEntrance: true)counts a flare's duration from the start of its entrance rather than its end, andHarborFlareEntry.durationcan be set after the raise, starting the time over with the new duration, as releasing a hold or coming back into sight does. (#101)HarborFlareEntry.maybeOf(context)is the flare a builder ortransitionBuilderis in, so a flare can take a hold on itself or read itsanchorandside.HarborAnchorClip(anchor:, side:)clips its child at the anchor's edge on that side, measured as it paints, for a flare that slides out from under the field it hangs from. An anchored flare never flips, so itssideis the one it hangs on. (#103)- Docs: a
HarborPortalBuoyshows in the frame it is built when its controller'sshow()is called ininitState, before the portal is attached; no post-frame callback is needed. (#102)
0.4.3+1 #
- Docs: the README, read again from start to finish. Sentences that still described the old corners now say a top or bottom dock spans only the part between the side docks.
entry.hold()is described as what it is, a hold you take and release yourself. The glossary gains clear water, waters, cast off, port and sheet. Examples now use widgets that compile, the any-height fling close is documented, a dock'scoast:and a harbor'ssizing:,coast:andminimum:are documented, and Accessibility follows the sections it refers to. The README's phone video is rendered again from current sources. No code changes.
0.4.3 #
- Fixed:
HarborSheet(clearsTopCoast: false)cast the status bar out of the sheet's clear water along with its header and body, so a flare or buoy raised in a full-height sheet could sit under the status bar. The header and body still read no top coast; the sheet's clear water, where flares, buoys andHarborChartlook, keeps the status bar out as it does with the option off. (#97) - Fixed: with no
HarborSea, aHarborCastOfforHarborMooredabove a harbor started from empty waters, so every edge read zero beneath it, not just the edges it cast off. A sheet withclearsSides: trueread a bottom inset of 0 instead of the home indicator (34 on an iPhone 17). It now starts from what a reader there sees,MediaQuery's padding. (#98)
0.4.2 #
HarborSheet(bodyClearsTide: false), and onHarborSheet.draggable, leaves the keyboard to the sheet's body, asHarbor.bodyClearsTidedoes a page's: the body runs under the keyboard and reads it inMediaQuery.viewInsets, a floating footer still rides up on it, andmaxExtentFraction(or a draggable sheet's extents) is a share of the whole height. For a toolbar or inspector that manages the keyboard itself.truestays the default, so nothing changes for existing sheets. (#95)- Fixed: a
Harborsized to its body (HarborSizing.hugBody) withmaxExtentFractionandbodyClearsTide: falseadded the keyboard's height to its cap, though its body leaves the keyboard outside its height, so with the keyboard up it grew past its fraction of the screen. The cap is now that fraction of the whole height. WithbodyClearsTide: true(every sheet until now) it is unchanged.
0.4.1 #
HarborDock.reserve(extent:)holds an edge for something drawn elsewhere (a footer in an overlay, a bar a parent paints) so the body keeps clear of it. It reachesextentin from the edge, coast included (the larger of the two) when it is the outermost dock, andextentpast the docks outside it when it is not. It paints nothing and takes no taps;kind:(a pier by default) andtide:are a dock's. The way to say this before, a pier with an empty child andminimum: extent, reserved nothing when another dock sat outside it, since only the outermost dock takes the coast and its minimum. (#92)- Docs:
HarborWaters.steadyCoastOfholds the home indicator while the keyboard is up, for a footer that keeps its height. A footer that sits on the keyboard readsMediaQuery.viewPaddingOfin a body that clears the tide: the indicator with the keyboard down, 0 with it up. (#93)
0.4.0 #
- Breaking: the side docks own the corners. A
startorenddock still runs the frame's full height, and atoporbottomdock now runs between the side docks instead of the full width under them, as a tablet'sNavigationRailsits beside itsAppBar. Measured on an iPhone 17 with an 80-wide start rail and a 16 margin: a header's mooring line starts at x 96, lined up with the body's rows, where before it was at 16, under the rail. A top or bottom dock beside a side dock is no longer handed that side's coast inMediaQuery.padding(the side dock took it), and keeps the coast on a side no side dock holds. A side dock that withdraws gives its corners back as it goes; one that widens on focus holds the header at itsrestingExtent. Migrating: a header that padded its title by the rail's width by hand (EdgeInsetsDirectional.only(start: railWidth)) drops that padding, or the title moves twice as far. A header that should paint under the rail (a full-bleed image across the top) paints that part in the body, behind the docks, withHarborOpenWater.
0.3.3 #
HarborFlares.raise(margin:)sets how far a flare keeps in from the clear water's edges: 16 at a slot or an alignment and 8 by an anchor when null, as before.EdgeInsets.zerogives a bar the width of the screen (a fixedSnackBar's look), still above the docks and off the coast; anEdgeInsetsDirectionalis resolved in the reading direction of the page that raised it. A bar that paints under the home indicator is a dock: moor it with aHarborPontoon. (#83)HarborFlareEntry.hold()stops a flare's time until the returnedHarborFlareHoldis released, for a toast under a finger or with focus in it. Holds are counted, as make-way claims are; time starts over once the last is released, as it does when a flare comes back into sight.HarborFlareEntry.heldsays whether one is on. (#84)- Fixed: a flare raised into an
Overlay(no harbor above it) and lowered before it was built stayed in the overlay and held its slot, so every later flare there waited behind it. It now leaves at once, unbuilt, andclosedcompletes with the reason it was lowered. (#85) - Fixed: with no
HarborSea, a harbor right inside the page's harbor (a header band's own) counted as a port, so a flare raised from it was shown inside that band. Without a sea, the page's outermost harbor is now its only port, as each route's first harbor is under a sea. (#86) - Docs:
HarborMakeWaysays that its claim reaches the harbor a frame after it is built, and how to make way in the frame a mode starts: claim withHarbor.of(context).makeWayin the tap that opens it, or build the dockstate: HarborDockState.withdrawn. (#82)
0.3.2+1 #
- Docs: the README links to the Harbor Field Guide in a browser (https://supposedlysam.github.io/harbor/), a pretend phone for every class with its options and a tide gauge. The example's field guide pages each say what to try, and keep their readings in view on a phone on its side. No code changes.
0.3.2 #
HarborSheet(clearsSides: true), and onHarborSheet.draggable, keeps the header, body and footer clear of the left and right coast (a phone's notch in landscape, a cutout on a side edge) and casts it off, so content beneath reads zero there and does not clear it again. The surface still runs edge to edge. Off by default: today a sheet's header and footer run its full width and are handed the side coast inMediaQuery.padding(measured on an iPhone 17 on its side, 62 each side: header and footer at x 0 to 874, padding 62 left and right), so turning it on by default would move existing sheets. PassclearsSides: trueto turn it on.showModalBottomSheet'suseSafeAreais off by default too. (#76)HarborSheet(clearsTopCoast: false), and onHarborSheet.draggable, keeps the geometryshowHarborSheet(keepsTopCoast: true)gives a sheet but casts off the top coast for its header and body, which read zero top padding there: a header that paints under the status bar and pads its own title no longer pads twice. It only matters withkeepsTopCoast: true; without it the sheet stops short of the status bar and is handed no top coast.truestays the default, so nothing changes for existing sheets. (#75)- Fixed:
HarborFairway(startsInOpenWater: true)dropped the fairway's ownpaddingandminimumat its leading end along with the clearance; it now skips only the clearance, so a first row givenpadding: EdgeInsetsDirectional.only(top: 16)sits 16 below the top (#74). - Docs: the README says how to keep a fairway off an end a header it does not own already cleared (
HarborCastOff(edges: {HarborEdge.top}), #58), and what a list that reads onlyMediaQuery.paddinggets and misses inside a harbor, withHarborFairway.paddingOfor an app-sideMediaQuery.copyWith(padding:)for the rest (#69). The note thatHarborClear.coastleaves out the keyboard, which sat among the fairway paragraphs, is withHarborMoored, which it describes. HarborWaters.dockFaceOf(context, edge): how far the docks on an edge reach, to their inner face, with or without a wake (#61). It is the wake band'sdockEdgewhere the edge has a fade wake, andHarborWatersData.docksotherwise, which is the face itself when the wake adds no clearance (no wake, a hairline, or a fade that rests at the dock's edge). It depends on the docks aspect alone, so its reader holds still while the keyboard moves. Inside a fairway it reads zero, as the fairway has cast its ends off. Additive.HarborFairway(tide:), onHarborFairway.box,HarborFairway.paddingOfandHarborFairwaySlivertoo, asHarborMoored(tide:)has it (#59).tide: falseleaves the keyboard to someone else: the bottom end and reveals leave it out, and it is not cast off, so content inside can still keep clear of it. A fairway withtide: falsedoes not rebuild as the keyboard moves. It replaces wrapping the fairway inHarborCastOff(tide: true), which also hid the keyboard from the content inside and, with the defaultedges, cast off all four edges (the keyboard-only form isHarborCastOff(edges: const <HarborEdge>{}, tide: true)). Additive:trueis the default, so nothing changes for existing callers.HarborFairway(clear:), onHarborFairway.box,HarborFairway.paddingOfandHarborFairwaySlivertoo, with the sameHarborClearandeverythingdefault asHarborMoored(#57). WithHarborClear.coastthe ends rest clear of the platform's insets alone, for a list that keeps its last row off the home indicator but runs under a frosted tab bar, or a carousel under a rail. The ends cast off the coast alone, asHarborCastOff(docks: false)does, so the rows beneath read the docks measured from the fairway's edge. Pinned sliver docks still pin at the docks' face, reveals still keep clear of everything over the viewport, and the bottom end still keeps clear of the keyboard. A fairway that clears the coast alone reads only the coast, so it holds still while the keyboard moves under a horizontal one. Additive: nothing changes for existing callers.- Fixed, and it can move content: with
HarborFairway(startsInOpenWater: true), the first sliver is handed back the leading end alone (#60). Before, it was given the fairway's ownMediaQueryand waters from above its cast-off, so it also read the trailing end (a tab bar, the home indicator), the keyboard, and on a horizontal fairway the trailing side and its mooring line, all of which the fairway had already cleared. A list whose only sliver is the first one cleared its bottom twice. Content in that sliver that readMediaQuery.padding,viewInsetsor the waters on the trailing end now reads zero there. - Fixed, and it can move content:
HarborFairwaySlivercasts off the ends it cleared, as Flutter'sSliverSafeArearemoves the padding it applied and as the README promised (#67). Before, it only padded, so aSafeArea, aHarborMooredor aMediaQuery.paddingreader in its sliver cleared the header, the status bar, the home indicator or the keyboard a second time. Content there that read those values now reads zero on the ends the sliver cleared (and no keyboard, when it cleared the bottom); an end givenclearLeading: falseorclearTrailing: falseis left as it was. HarborFleetObserverhears harbors join and leave, as aNavigatorObserverhears routes:didJoinanddidLeaveare given the harbor'sHarborController, after the frame it joined or left in. Register one withHarborSea(observers:)orHarborFleet.addObserver(andremoveObserver), on the fleet fromHarbor.of(context).fleet. (#64)HarborChart.snapshotAll()reads every harbor of every live sea with no context, for tooling outside the widget tree. A sea inside another harbor (a phone drawn in a page) is listed too, andHarborChartEntry.isolated(also in itstoJson()) says so;ext.harbor.chartleaves isolated seas out unless called withisolated=true. Fixed: the VM-service extension kept the last sea it was given after that sea was disposed. It now reads the seas that are mounted. (#64)HarborWake.fade(dockOpacity:, curve:)shapes a fade's ramp: how opaque content is at the dock's inner face (0.25 by default) and the curve out to the wake's end (Curves.linearby default). The defaults draw the ramp every fade drew before. The measured band carries them asHarborWakeBand.dockOpacityandHarborWakeBand.curve(null for a fade at its defaults, soHarborWakeMask(dockOpacity:)still applies there), and they are part of the band's and the wake's==,hashCodeand diagnostics. A wake takes no colour: it is an alpha mask, so it shows whatever is behind the content. (#70)HarborTideSource(height:, child:)feeds a keyboard the platform does not report (a plugin's, a TV's over a channel) into the tide: it rebuilds theMediaQuerybelow it withviewInsets.bottomat the larger of the platform's value andheight(logical pixels), and rebuilds nothing else whenheightchanges. Mount it aboveHarborSea, and aboveHarborScaleModel. The README's new section "A keyboard the platform does not report" has the same as aMediaQueryrecipe. (#71)- Docs: how to build a dock of a given reach, coast included, with no helper:
HarborDock.quay(minimum: reach, child: const SizedBox.shrink())reaches the larger of the coast andreach, andcoast: HarborCoastStance.nonewithSizedBox(height: reach)reaches exactlyreach. In the docks table and harbor_test's README. (#68) showHarborDialog(transitionBuilder:)brings your own entrance and exit in place of the 180 ms fade, asshowGeneralDialog's does. It is handed the route's animation curved byanimationStyle; under reduced motion it is handedkAlwaysCompleteAnimation(andkAlwaysDismissedAnimationas the secondary), so reduced motion stays in charge. Null keeps the fade. (#63)HarborDialogRoute<T>, aRawDialogRoutewithshowHarborDialog's options, is public, as Material'sDialogRouteis:showHarborDialognow pushes one, with no change in behaviour, and you can push one yourself to keep the route or choose the navigator. It reads the opening page's themes (unless giventhemes:) and, withinheritClearWater, its clear water when it is installed, as it is pushed. (#63)HarborPortalBuoy(requestFocus: true)takes keyboard focus as it opens, as a sheet with no barrier does: the buoy is a focus scope of its own that becomes the first focus of the scope around its anchor, Tab past its last control follows the navigator'srouteTraversalEdgeBehavior(a portal buoy is not modal, so it does not loop), and when it closes focus goes back to where it was if it is still in the buoy. It asserts anonDismiss, so Escape from inside it closes it, before a modal buoy it was opened from.falsestays the default, which leaves focus where it was, as aMenuAnchordoes. (#66)HarborFlares.raise(anchor:, side:, gap:, crossAlignment:)raises a flare by aHarborAnchorPoint, placed as aHarborBuoy.anchoredis (below, 8 away and centred by default): "Copied" by the button that copied. The anchor is given alone, without a slot or an alignment (asserted). An anchored flare is shown by the port of the page that raised it rather than the one on top, takes turns with the flares at the same anchor and side, is hidden with its time stopped while the anchor is out of the tree, and is lowered withHarborFlareClosedReason.removeif its page is popped, rather than moving to the port now on top. With no harbor it sits by its anchor in the nearest overlay. It keeps the live region and dismiss action.HarborFlareEntryreads backanchor,side,gapandcrossAlignment. (#65)- Docs: the README's sheets section has a recipe for a page under
PopScope(canPop: false): thereNavigator.maybePopstops before the page's local history, so back reaches only the app's handler and never a modal buoy, portal buoy or sheet with no barrier. The handler closes them first withNavigator.popwhileModalRoute.of(context)!.willHandlePopInternally, one per back press. No code changes. (#62)
0.3.1+1 #
- Docs: the README's videos play in the browser when clicked, instead of downloading. They are linked through jsDelivr, which serves them as video. No code changes.
0.3.1 #
- Behaviour change: a sheet closes on a fling down faster than 700 logical px/s, the speed at which a Material
BottomSheetcloses, and the speed is now a parameter:HarborSheet.draggable(closeFlingVelocity:)andHarborSheet(closeFlingVelocity:). A draggable sheet's header used to close it on a fling of 400 from its lowest snap, so a moderate flick (between 400 and 700) that used to close a draggable sheet now settles it back at that snap. To keep the old feel, passcloseFlingVelocity: 400. A content-sized sheet withdragToClosealready closed at 700 and is unchanged.double.infinityleaves only the floor to close a sheet. A fling on a draggable sheet's list is stillDraggableScrollableSheet's own, as in a modal bottom sheet: it closes the sheet from its lowest snap at any speed. (#48) - Signals are now flares, so a toast is not mistaken for the
signalspackage's reactive state:HarborFlares.raise,HarborFlareSlot,HarborFlareTarget,HarborFlareEntry,HarborFlareTransitionBuilder,HarborFlareClosedReasonandHarborController.flares. Not breaking: the old names (HarborSignals,HarborSignalSlot,HarborSignalTarget,HarborSignalEntry,HarborSignalTransitionBuilder,HarborSignalClosedReasonandHarborController.signals) still compile as deprecated aliases, with a warning naming the new one, and go at 1.0. The message reported when a flare has nowhere to go now namesHarborFlares.raise. HarborCastOff(docks: false)casts off the coast alone and leaves the docks to what is beneath, for a layer that keeps its child clear of the status bar and home indicator while a list inside it still rests clear of a frosted header or tab bar (#47). Beneath, the docks, the docks at rest, their wakes andMediaQuery.paddingandviewPaddingare measured from the layer's edge, and the steady coast is zero.docks: truestays the default, so nothing changes for existing callers.HarborFairwaygets no such option: it casts off only its own ends, so the docks across a horizontal fairway already reach its items.- The first line of
HarborDock.pier,HarborDock.quayand eachHarborTideStance, which an editor's hover shows, says what it does in plain words and the Flutter widget it matches: the body runs under a pier, as under an app bar withextendBodyBehindAppBar, and stops at a quay, as atScaffold.appBar; afloatdock rides up on the keyboard,pilingsstays put under it, anddryDockkeeps its space whether it is up or down.
0.3.0 #
Breaking changes #
- Every inset is an
EdgeInsetsGeometry, asPadding.paddingis:EdgeInsetsDirectionalvalues still compile; code that reads an inset back as a directional type needsHarborEdges.resolve. - Dialogs are popup routes (
RawDialogRoute, asshowGeneralDialog's): aHerono longer flies into one and aRouteObserver<PageRoute>no longer sees one as a page. - A modal
HarborBuoyholds keyboard focus as a dialog route does: it takes focus when it opens, keeps Tab inside it, closes on Escape, and gives focus back when it goes. - Defaults with no theme of their own: sheet and dialog barriers are
showGeneralDialog'sColor(0x80000000), a hairline wake is a translucent black, and every barrier harbor puts up is announced as'Dismiss'unless given a label. - Internals are behind the public handle:
Harbor.of(context)replaces constructing aHarborController, its lifecycle methods are@internal, pontoons take a typedHarborPontoonHandle, and dock records are read throughHarborChart.nearest. - Behaviour that moves things:
HarborBuoySide.before/afterare deprecated aliases ofstart/end(so.nameis'start');HarborFairway.keyboardDismissBehaviorfollows the scroll behaviour when unset; signals at one slot take turns and time only while in sight (4 s by default); a no-barrier sheet comes back after the page over it has finished popping; a fling down on a sheet's header from its lowest snap closes it.
Everything in this release #
-
Fixed: a sheet with no barrier that took focus, opened over a modal buoy, left focus on its own disposed scope when it closed, so Escape no longer reached the buoy. It now hands focus back as it closes, as a route does.
-
Breaking: a modal
HarborBuoyholds keyboard focus as a dialog route does: it is aFocusScopethat takes focus when it opens, keeps Tab inside it and stops the arrow keys at its edges, and gives focus back to the page when it closes. Escape calls itsonDismiss, from focus in the buoy or on the page behind it (not from a page of aNavigatornested in the harbor's body, whose route answers Escape first); with no modal buoy up, Escape is left to anActionsabove the harbor.HarborBuoy(requestFocus: false)leaves focus where it was, asRoute.requestFocusdoes. Opened over a focused text field, it now takes focus from the field, so the keyboard goes down as it does for a dialog; passrequestFocus: falseto keep it. Before, focus stayed on the page behind the barrier, Tab and a TV remote's D-pad walked the page, and Escape did nothing. -
A buoy's child keeps its state when the buoy turns
modalor back (a speed-dial button that is modal while it is open), when another buoy turns modal or stops being modal, and when a signal comes up in its harbor. Before, each of these built every buoy's child again from scratch. -
Breaking: a harbor's handle (
HarborController) carries only what content asks of a harbor, asScaffoldStatedoes; the harbor itself makes it and runs its lifecycle. These members are now@internal, so using them from another package is an analyzer warning (invalid_use_of_internal_member), and they may change in any release:- the
HarborController(...)constructor: get a harbor's handle withHarbor.of(context); onChanged,join(),leave(),recordLayout()andraiseSignal(): raise a signal withHarborSignals.raise(which aims at the port on top or at the sea; a nested harbor that is not a port is no longer a target of its own);- the setters of
debugLabel,dockedEdgesandroute, which stay readable (dockedEdgesnow returns an unmodifiable set); - the
HarborSignalEntry(...)constructor:HarborSignals.raisereturns the entry.
- the
-
Breaking:
HarborController.lastLayoutandrenderBoxare deprecated, read-only getters; setting either no longer compiles. ReadHarborChart.nearest(context), which has the harbor's frame, body, clear water and docks in global coordinates, orclearWater/clearWaterInGlobal(). -
Breaking:
HarborController.addPontoonreturns aHarborPontoonHandle, andupdatePontoonandremovePontoontake one, where they took an untypedObject. A handle stored asObjectneeds its type changed; the handle reads back itsedgeanddock. -
Breaking:
HarborSignalEntry.showingis aValueListenable<bool>, asNavigatorState.userGestureInProgressNotifierreads;HarborSignalEntry.owneris read-only. Lower a signal withlower()(which takes theHarborSignalClosedReasonthatclosedreports) rather than settingshowing.value. -
HarborDockSlotis deprecated and its constructor@internal: it is how a harbor builds a dock, and stops being exported in a later release. Find where docks are withHarborChart.nearest(context).docks(docksAroundinharbor_test), or find the dock's child. -
Harbor.of(context)andHarbor.maybeOf(context): the nearest harbor's handle, asScaffold.ofreturns the nearestScaffoldState. LikeHarborController.of,Harbor.ofthrows aFlutterErrornaming itself when there is no harbor above the context.HarborController.ofandmaybeOfstill work. -
HarborChart.nearest(context): the chart entry of the nearest harbor, in global coordinates, or null when there is none or it has not laid out. -
Breaking: harbor's drawn defaults carry no theme of their own, as the widgets layer's do.
showHarborSheetandshowHarborDialogshareshowGeneralDialog's barrier colour,Color(0x80000000)(the sheet's was0x66000000, the dialog's0x88000000), andHarborWake.hairline(), which everyHarborSheetfooter uses by default, is a translucent black,Color(0x1F000000), instead of a translucent white that vanished on a light bar. PassbarrierColor:orHarborWake.hairline(color:)to keep the old look. -
Breaking: a sheet or dialog barrier given no
barrierLabelis announced as'Dismiss'instead of'Close sheet'or'Close dialog'(0.2.0's), the English that Material's and Cupertino's localizations fall back to formodalBarrierDismissLabel. A test that finds the barrier by'Close sheet'or'Close dialog'looks for'Dismiss'; an app that localizes passes its own label, as before. -
Escape closes a sheet with no barrier, as back does: from focus in the sheet, or in a harbor on the page that opened it. Only while such a sheet is up, as
RawMenuAnchoranswers Escape only while its menu is open, so anActionsabove the page still gets Escape otherwise. Before, Escape did nothing while the sheet was up. -
A sheet with no barrier is a
FocusScopeof its own, with its navigator'srouteTraversalEdgeBehavior, as a route's scope is. With the default, Tab goes through the sheet and on to the page as before; under a navigator that keeps Tab inside each route, it now keeps Tab inside the sheet instead of leaving it for the page for good.FocusScope.of(context)inside the sheet now returns the sheet's scope, not the navigator's. -
showHarborSheet(requestFocus:), asshowModalBottomSheettakes it. A sheet that is a route passes it toRoute.requestFocus; a sheet with no barrier leaves focus where it was unless it is true, as a persistent bottom sheet does, and gives focus back to the page when it closes. -
HarborPortalBuoy(onDismiss:, consumeOutsideTaps:): a portal buoy closes as aMenuAnchordoes. Its buoy and itschildare oneTapRegiongroup, so a tap outside both callsonDismissand a tap on the child is left to the child; Escape with focus in either and back call it too, back before it reaches the page.consumeOutsideTaps: truekeeps that tap from also pressing what is under it. WithoutonDismissnothing changes. -
HarborBuoy(barrierLabel:): what a screen reader announces for a modal buoy's barrier,'Dismiss'when none is given, as for sheets and dialogs (it was'Close'). A Material app passesMaterialLocalizations.of(context).modalBarrierDismissLabel. -
crossAlignment:andcrossOffset:onHarborBuoy.anchoredandHarborPortalBuoy: an anchored buoy lines up with its anchor'sstartorendacross its side (in reading order above and below it, top and bottom beside it) instead of centring on it, and is moved on bycrossOffset, still held inside the clear water.HarborBuoyCrossAlignment.centeris the default. -
HarborPortalBuoy.sideOf(context)andmaybeSideOf: the side a portal buoy landed on, itssideor the opposite after a flip, so a popover can point its arrow at its anchor. It changes on the frame after the flip. -
The README's glossary gains a table that gives each harbor term its closest Flutter concept (
SafeArea,MediaQuery.paddingandviewInsets,Scaffold,showModalBottomSheet,SnackBar,OverlayPortaland more) and the difference that matters, or says there is none. The main classes' dartdoc points to the same Flutter widget, andHarborTideStateandHarborDockStatesay they are notStateobjects. Documentation only. -
Signals at the same slot or alignment of one port take turns, as a
ScaffoldMessengershows its snack bars, instead of being drawn over each other: each comes in once the one before it has run its exit, and one lowered while it waits leaves without being shown. Signals at different slots still show together. This is a behaviour change for code that raised two signals at one place and expected both on screen. -
A signal's
durationcounts only while it is in sight: from the end of its entrance, and not while another route covers its page, as a snack bar's timer starts only once it is in and its route is current. The default is 4 s, aSnackBar's, instead of 3 s. AHarborSignalEntryraised throughHarborController.raiseSignalnow times out after itsdurationtoo. -
HarborSignals.raise(persist: true)keeps a signal up until it is lowered, asSnackBar(persist:)does, for a signal with a button that a screen-reader user needs time to reach. -
HarborSignalEntry.closedcompletes with aHarborSignalClosedReason(lower,dismiss,timeout,remove) once the signal has left, asshowSnackBar's controller'scloseddoes;lower()takes the reason, ashideCurrentSnackBardoes. -
An anchored buoy or a portal buoy that is not shown, because its anchor is not in the tree, is not read out by screen readers. It was left in the semantics tree where it last sat (at first, the top-left corner), so a screen reader could reach an invisible menu or bubble. A portal buoy's semantics also follow it when its anchor moves; they stayed where it was first placed.
-
HarborBuoySide.startand.endname an anchored buoy's reading-order sides, asAlignmentDirectional.centerStartandCrossAxisAlignment.startdo.HarborBuoySide.beforeand.afterare deprecated aliases of them and place the buoy exactly as before; a side'snameis now'start'or'end'. -
Breaking: because
beforeandafterare now aliases,HarborBuoySide.before.nameis'start', andHarborBuoySide.values.byName('before')throws. Code that stores a side by name should store'start'or'end'. -
harbor's builders have named types, as Flutter's do:
HarborOpenWater.builderis aHarborWatersWidgetBuilder, andHarborSheet.draggable(builder:)is aScrollableWidgetBuilder, asDraggableScrollableSheet.builderis. The function types are unchanged, so existing builders still fit. -
HarborDock(animationStyle:)andHarborLighthouseRegion(animationStyle:)take anAnimationStyle, as Flutter's routes andMaterialApp.themeAnimationStyledo, so leaving and returning can differ. A dock withdraws and goes dark over itsreverseDurationandreverseCurveand returns over itsdurationandcurve; a region lifts over the forward pair and settles back over the reverse one.AnimationStyle.noAnimationmakes either move at once. Given, it overridesduration:andcurve:, which stay. -
Breaking:
HarborFairway.keyboardDismissBehavioris nullable and unset by default, as on aScrollView: it follows the fairway'sscrollBehavior, or else the inheritedScrollConfiguration. Before, it was alwaysmanual, which overrode an app-wideScrollBehaviorthat dismisses the keyboard on drag. PassScrollViewKeyboardDismissBehavior.manualto keep the old behaviour under such a behaviour. -
Two slivers in one
HarborFairwaywith the same key now trip Flutter's duplicate-key assertion, as they do in aCustomScrollView: a fairway keeps keyed slivers' state when they move, which a duplicate key cannot. Before, it tolerated them. -
HarborFairwaytakes the rest ofCustomScrollView's parameters, with the same defaults:scrollBehavior,center,anchor,paintOrder,dragStartBehavior,restorationIdandhitTestBehavior.HarborFairway.boxtakes those aSingleChildScrollViewhas. With acenter, both ends of the scroll rest clear of the docks, the center sliver rests clear of the leading docks, sliver docks after it pin at the docks' face, and reveals on either side of it keep clear.anchoris a fraction of the water between the docks, soanchor: 1follows the keyboard, and reveals account for it. -
Keyed slivers in a fairway keep their state when they move, as in a
CustomScrollView. -
Breaking: every inset harbor takes is an
EdgeInsetsGeometry, asPadding.paddingandListView.paddingare, resolved againstDirectionalitywhere it is used:Harbor.marginand.minimum,HarborSea.margin,HarborFairway.paddingand.minimum,HarborFairwaySliver.paddingand.minimum,HarborMoored.minimumand.extra,HarborCoast.fixed(andHarborCoast.fixedInsets) andHarborTitleSafe.fixed, and theextra:andminimum:ofHarborFairway.paddingOfandHarborMoored.clearanceOf.EdgeInsets.all(16)now compiles, and anEdgeInsetsstays on the side it names. PassingEdgeInsetsDirectionalstill compiles and behaves as before; reading one of these fields asEdgeInsetsDirectionalneedsHarborEdges.resolve(insets, textDirection), andHarborTitleSafe.resolve(size)now returns anEdgeInsetsGeometry. What harbor publishes for you to read (HarborWatersData,HarborMoored.clearanceOf's result) stays directional. -
A breakwater sheet's coverage follows the sheet as it is drawn. While the sheet slid in or out, or was dragged, the page under it was laid out against where the sheet had been painted in the frame before, so its content trailed the sheet's edge by a frame (over 80 px at the fastest point of a 300 px sheet's slide). A sheet with no barrier also stopped covering the page as soon as it began to close, so the page sprang back under the sheet still sliding out; it now keeps clear until the sheet has gone, as it already did under a sheet with a barrier.
-
harbor's widgets describe their settings to the widget inspector and
debugDumpApp(debugFillProperties), as Flutter's own do, hiding the ones at their defaults.HarborDock,HarborBuoy,HarborWake,HarborCoast,HarborTitleSafeandHarborSheetExtentareDiagnosticable, so they print asHarborDock.pier(state: dark, debugLabel: "header")rather thanInstance of 'HarborDock', and aHarborAnchorprints itsdebugLabel. TheirtoStringnow takes Flutter's{DiagnosticLevel minLevel}, so a subclass of one of them that overridestoString()needs that parameter. -
HarborSheetExtenthas value equality (==andhashCode), as its@immutablepeers already did. -
With reduced motion (
MediaQuery.disableAnimations),HarborLighthouse.reveal, and so aHarborBeacon(keepInSight: true), jumps to where it reveals instead of scrolling there over itsduration, and aHarborLighthouseRegionlifts its content at once, as docks, sheets, signals and dialogs already appear. A region also takes a newdurationgiven after it is first built. -
Misuse is reported as a
FlutterErrorthat says what to do, as Flutter's own errors are.HarborController.ofwith no harbor above throws one in release builds too (before, a bare null-check error), and points toHarborController.maybeOf. A harbor given unbounded constraints, a quay listed inside a pier andHarborSheet(dragToClose: true)outsideshowHarborSheetname the harbor'sdebugLabeland the fix in debug builds, instead of failing a plain assert. -
In debug builds, two
HarborAnchorPoints that stay attached to oneHarborAnchorpast the end of a frame are reported. Before, the buoy silently moved to whichever laid out last. -
showHarborSheet(sheetAnimationStyle:): anAnimationStyle, as onshowModalBottomSheet, sets how a sheet opens and closes, with or without a barrier:durationandcurvegoing in,reverseDurationandreverseCurvegoing out, andAnimationStyle.noAnimationfor none. A dragged sheet follows the finger with no curve and goes on along the curve when let go, so it stays under the finger with any curve, one that overshoots included. Reduced motion still wins. -
HarborSheetController, given toshowHarborSheet(controller:): closes a sheet from outside it (close(),remove()with no slide), completesclosedwhen it has left, rebuilds it (setState) and reads its slide (animation). It is aChangeNotifierthat tells its listeners when a sheet attaches and leaves. For a sheet with no barrier, whichcloseHarborSheetcould only close from inside, and for one with a barrier. -
showHarborSheet(transitionAnimationController:), as onshowModalBottomSheet: the sheet slides by the caller's controller in place of its own, and the caller disposes it. -
HarborSheet.draggable(controller:)takes aDraggableScrollableController, so a draggable sheet can be read and moved from outside it: fitted to content measured after layout, or raised to its ceiling when a field takes focus. -
HarborSheet.draggable(expand:), as onDraggableScrollableSheet:falsein a route that sizes the sheet to its content (showModalBottomSheet), so a tap above the visible sheet reaches the barrier and closes it. Before, the sheet filled the modal bottom sheet's surface and swallowed those taps. -
HarborSheetExtent(shouldCloseOnMinExtent:), as onDraggableScrollableSheet: off, a draggable sheet rests at its floor instead of closing. -
A fling down on a draggable sheet's header from its lowest height goes to its floor and closes it, as a fling on its list does. Before, the header sprang back to its rest unless flung faster than 1200. A slow release still goes to the nearest of its heights.
-
HarborSheet.close(context, [result])closes the sheetcontextis in and completes the future that opened it withresult, asNavigator.pop(context, result)does for a modal bottom sheet. A sheet with no barrier can now return a value: before, its future always completed with null. Back andNavigator.popstill close it with null, since it is not a route. -
closeHarborSheetis deprecated in favour ofHarborSheet.close, andharborAlphaWakein favour ofHarborWakeMask.alphaWake. Both old names still work. -
showHarborSheet(isDismissible:, requestFocus:, anchorPoint:), asshowModalBottomSheethas them. WithisDismissible: falsea tap on the barrier (dimmed or clear) does nothing and back still closes the sheet; before, every barrier closed its sheet on a tap.requestFocusis passed to the sheet's route, andanchorPointpicks the screen a sheet opens on beside a hinge. -
Fixed: under a body that clears the tide, on a phone with a home indicator,
HarborMoored(clear: HarborClear.coast)and every reader ofHarborWaters.of(context, aspect: HarborWatersAspect.coast)rebuilt on every frame of the keyboard, and so did a reader ofHarborWaters.steadyCoastOf. A coast reader now rebuilds only when the coast changes, andsteadyCoastOfwhen the view padding does, asMediaQuery.viewPaddingOfwould.HarborWaters.of(aspect: HarborWatersAspect.coast).coastSteadyis filled in without subscribing, as the docks aspect already did; read it withsteadyCoastOfto follow it. Values are unchanged. -
A tap on the iOS status bar scrolls only the page a
Scaffoldwould: the one whose status bar band a tap at the screen's top left reaches. A page in a nested navigator no longer scrolls once a route covers that navigator (it scrolled to the top when uncovered), nor does a page under a full-screen overlay entry or the right-hand pane of a split view. A harbor with no top inset, and one inside a sheet that stops short of the status bar, no longer scrolls either, as aScaffolddoes not. Nor does a harbor whosecoastdrops the top inset: the band is as tall as the top coast the harbor keeps. -
Breaking:
showHarborDialogpushes a popup route (aRawDialogRoute, asshowGeneralDialogdoes) instead of a page route. AHerono longer flies into a harbor dialog, aRouteObserver<PageRoute>(and analytics observers that count page routes as screens) no longer sees one as a page, aHarborSheet.draggableinside one closes it when flung below its floor, and its content is a route scope for screen readers. The API is unchanged; code that checkedroute is PageRoutefor a harbor dialog no longer matches. To migrate: checkroute is PopupRoute(orRawDialogRoute); name the dialog withrouteSettingsfor screen analytics; for aHerointo a dialog, push aPageRouteof your own. -
showHarborDialogtakesshowDialog's route options:routeSettings:,barrierLabel:('Dismiss'when none is given; a Material app passesMaterialLocalizations.of(context).modalBarrierDismissLabel),semanticLabel:(the name screen readers announce for the dialog),anchorPoint:,traversalEdgeBehavior:,requestFocus:andanimationStyle:. -
showHarborDialogmoved fromscale_model.darttodialog.dart. It is still exported frompackage:harbor/harbor.dart. -
A
HarborBeacon(keepInSight: true)also brings itself into sight when focus moves into it, asEditableTextdoes for its caret. In a form with the keyboard up, tapping the next field or pressing the keyboard's next action now reveals the whole beacon (the field and the button under it,clearanceclear) rather than only the field's caret line. Before, a beacon re-revealed only when the keyboard rose or what covers the bottom grew. This applies with the keyboard down too. Every beacon now holds aFocusnode of its own, soFocus.of(context)inside a beacon returns the beacon's node. -
HarborSignals.raise(animationStyle:, transitionBuilder:): a signal's entrance and exit, asshowSnackBar(snackBarAnimationStyle:)andshowGeneralDialog(transitionBuilder:)take them.AnimationStyle.noAnimationshows a widget that brings its own entrance as it is, instead of fading and scaling it in on top; atransitionBuilderbuilds the entrance and exit from harbor's animation. A lowered signal stays for its whole exit, and never less than the 300 ms it stayed before. -
A signal's live region has a dismiss action, as a
SnackBar's does, so a screen reader can lower it.HarborSignals.raise(liveRegion: false)leaves the semantics to a widget that is its own live region: before, harbor wrapped it in a second live region with no label. -
HarborSignalEntrygainsanimationStyle,transitionBuilder,liveRegionandlingers, the time a lowered signal stays in the tree;HarborSignalTransitionBuilderis exported. -
HarborSignals.raise(alignment:): raises a signal at an exact point in the clear water instead of a slot, placed as a buoy at that alignment would be, and in the nearest overlay's padded water when there is no harbor. AnAlignmentDirectionalis resolved in the reading direction of the page that raised it.slotis now nullable and still defaults tohigh; give one or the other. A tear-off ofraisestored in a variable typed with a non-nullableslotneeds its type updated. -
A sheet with a barrier is a semantics scope of its own, as a modal bottom sheet is: it scopes and names its route (
scopesRoute,namesRoute,explicitChildNodes), so screen readers keep to the sheet and announce it as it opens.showHarborSheet(semanticLabel:)is the name they announce. A semantics-tree snapshot of an open sheet gains this node. A sheet with no barrier is not a route and is unchanged. -
A sheet's barrier is read by a screen reader only above the sheet, as a modal bottom sheet's is (
ModalBarrier.clipDetailsNotifier): touch exploration over the sheet finds its content rather than the barrier. The clip follows the sheet's top as it slides and is dragged, and stops at a draggable sheet's top rather than its whole drag area. A semantics-tree snapshot of an open sheet shows the barrier's node ending at the sheet. -
showHarborSheet(barrierOnTapHint:): what tapping the barrier does, read as 'Double tap to …', as onModalBottomSheetRoute. A Material app passeslocalizations.scrimOnTapHint(localizations.bottomSheetLabel). -
An anchored buoy and a portal buoy follow their anchor in the frame it moves, as
OverlayPortal.overlayChildLayoutBuilderdoes. An anchor in a list row that scrolled was moved without being painted again, since each row is its own layer, so the buoy stayed where it was until something else repainted it; where the row was painted again, the buoy followed a frame late, and asked for an extra frame to do it. Each is now its own layer and is painted again at the start of every frame that is drawn, asking for no frame itself. AHarborAnchorwith no listeners of its own no longer schedules frames as it moves. -
An anchored buoy whose anchor is not in the tree takes no taps. It was already not painted, but it was still hit-tested where it last sat (at first, the top-left corner), so an invisible buoy could swallow taps meant for the page. A portal buoy already behaved this way.
-
Breaking: a sheet with no barrier comes back after a page pushed over its own has finished popping, not as the pop starts. Before, it was painted and tappable over the leaving page for the whole transition.
0.2.0 #
Breaking changes #
- Sea trials moved to
harbor_test. harbor no longer depends onflutter_test. Runflutter pub add dev:harbor_testand importpackage:harbor_test/harbor_test.dart;package:harbor/testing.dartis now empty and deprecated. HarborBuoy(modal: true)is modal and needsonDismiss: a barrier keeps taps off the page, and a tap beside the buoy or back callsonDismiss.HarborBuoy.alignmentis anAlignmentGeometryandHarborBuoy.marginanEdgeInsetsGeometry. PassingAlignmentandEdgeInsetsstill compiles; reading the fields as the physical types needsresolve(textDirection).- Behaviour that moves things: an anchored buoy's
beforeandafterfollow reading order under right-to-left; a sheet with no barrier closes on back before its page; on a dual-screen device, sheets, dialogs, signals and buoys keep off the hinge; a signal raised with no harbor above it shows in the nearest overlay instead of asserting.
Everything in this release #
-
Breaking:
HarborBuoy(modal: true)is modal. It puts a barrier over the page and its docks (barrierColor, clear by default), a tap beside it or back calls the newonDismiss, and screen readers leave the page alone while it is up; it still hides the buoys listed before it.onDismissis required withmodal: true. Before, a modal buoy only hid the buoys before it, and taps and back reached the page. -
Sheets, dialogs, signals and buoys keep off a foldable's hinge, as Material's dialogs and bottom sheets do: each is kept to one screen. A flat fold, which has no width, may still be spanned.
HarborTrialDevicegainsdisplayFeatures;foldableOpendeclares its fold, and the newdualScreenOpena hinge. -
A dialog's builder sees the opener's themes, and a dialog appears without fading under reduced motion, as sheets and signals already do.
-
A sheet with no barrier is tied to the page that opened it: back closes it before the page, the iOS back swipe stands aside while it is up, it hides while another page is on top, and it leaves when its page is replaced. Before, back popped the page from under it, and opened from above the page's harbor it could outlive the page.
-
After a turn or a foldable opening, a page under a breakwater sheet is no longer laid out for one frame against the cover the sheet had in the old shape (it could overflow), and a signal follows the clear water of the harbor that raised it instead of staying boxed into the old screen.
-
HarborSignals.raisewith no harbor above the context shows the signal in the nearestOverlay, clear ofMediaQuery.paddingandviewInsets, instead of asserting in debug and showing nothing in release. With no overlay either, it reports aFlutterError. -
A signal's timers are cancelled when it is lowered or its harbor leaves with nowhere to move it, so a widget test that removes the tree with a signal up no longer fails with a pending timer.
-
Readers rebuild only for what they read.
HarborWaters.ofdepends onMediaQuery's padding (and, outside a harbor, its size) rather than all of it, and only where itsaspectneeds them.HarborTide.isInOfrebuilds when the keyboard comes or goes, not on every frame it moves.HarborMoored,HarborMooringLine,HarborOpenWater,HarborDryDockand a horizontalHarborFairwayno longer rebuild while the keyboard animates. Layout is unchanged. -
HarborWaters.steadyCoastOf(context, edge)andHarborWatersData.coastSteady: the coast as it is with the keyboard down, so a footer keeps the home indicator's height while the keyboard is up. -
HarborFairway(minimum:),HarborFairwaySliver(minimum:)andHarborFairway.paddingOf(minimum:): a floor on each end's clearance, as onSafeArea. -
HarborFairway.boxtakes its child's size across the scroll when that axis is unbounded, so a horizontal row in aColumnis as tall as its content instead of throwing. -
Docs: mooring the bottom edge alone clears the coast and the keyboard but not a header;
HarborCoastFeature.hingeandHarborController.isPortare reserved and not yet read. -
showHarborSheet(routeSettings:): the sheet's route carries its settings, so navigator observers and route-name analytics see it. -
showHarborSheet(barrierLabel:): what a screen reader announces for the barrier,'Close sheet'when none is given. A Material app passesMaterialLocalizations.of(context).modalBarrierDismissLabel. -
HarborSheet(contentBuilder:): wraps the header, body and footer above the surface. A Material app wraps them in a transparentMaterialso text fields and ink work (the README has the recipe, with the bottom sheet theme's width). -
harbor's core imports no design library, and a test keeps it that way: Flutter 3.47 moved Material and Cupertino into
material_uiandcupertino_ui. -
HarborSheet(clip:): clips the sheet to a shape, so an edge-to-edge body follows the surface's rounded top. -
HarborSheet(dragToClose: true): a content-sized sheet can be dragged down to close. Off by default. -
HarborSheetExtent(snapSizes:): the heights a draggable sheet snaps to. A fling on its header goes to the next one its way, as a fling on its list does. -
Fixed: a draggable sheet in a route
showHarborSheetdid not open (showModalBottomSheet,showGeneralDialog) did nothing when dragged below its floor; it now closes that route. -
HarborPortalBuoy: an anchored buoy opened from anywhere in the tree (a menu from a list row, a popover from a button), built onOverlayPortal. It is placed in the nearest harbor's clear water by its child or aHarborAnchor, flips to the other side of its anchor when its side has no room, and is held inside the clear water otherwise. -
HarborBuoySide.beforeand.afterare in reading order: under right-to-left, an anchored buoybeforeits anchor sits on its right. They were placed as if left-to-right. Accessibility and Flutter's conventions for custom widgets. -
A dock that is dark or withdrawn is skipped by keyboard focus and by screen readers. Before, Tab still reached a dark dock's controls, and a withdrawn dock was still read out and focusable.
-
A signal is a live region, so screen readers announce it as it appears.
-
A signal keeps the themes of the place that raised it, as a sheet does.
-
A sheet's builder sees the opener's themes in its own
context. Before, they reached only the widgets below what the builder returned. -
Docks, signals and sheets honour
MediaQuery.disableAnimations: with reduced motion they appear and leave without moving. -
On iOS, a tap on the status bar scrolls a harbor page's primary scroll view to the top, as it does under a
Scaffold. -
Breaking:
HarborBuoy.alignmentis anAlignmentGeometry, andHarborBuoy.marginandHarborPortalBuoy.marginareEdgeInsetsGeometry, soAlignmentDirectional.bottomEndplaces a buoy by reading direction. Code passingAlignmentandEdgeInsetsstill compiles; code reading.alignmentor.marginas the physical types needs aresolve(textDirection). -
Breaking: sea trials moved to their own package,
harbor_test, so harbor no longer depends onflutter_testand an app that depends on harbor no longer gets the test framework in its own dependencies. Addharbor_testas adev_dependencyand importpackage:harbor_test/harbor_test.dartin place ofpackage:harbor/testing.dart.pumpSeaTrial,HarborTrialDeviceandisInClearWaterare unchanged. -
package:harbor/testing.dartis now empty and deprecated: the analyzer reports where it is imported, with a pointer toharbor_test. It is removed in the next release.
0.1.0 #
The first harbor.
Harbor,HarborSea: frames whose edges docks are built against, laid out in one pass (docks, then the body, then the buoys).HarborDock.pier/.quay: measured docks that content sails under or starts beyond; wakes (HarborWake.fade,.hairline), tide stances (float, pilings, dry dock), states (open, dark, withdrawn) with extent policies, resting extents, minimums.HarborCoast: the platform's insets, fixed coasts,HarborCoast.none, and TV title-safe.HarborTide,HarborDryDock: the keyboard's height after it is consumed, its phase and its high-water mark.- Content:
HarborMoored,HarborMooringLine,HarborFairway(vertical and horizontal, reveals widened by what covers the edge),HarborFairwaySliver,HarborSliverDock,HarborOpenWater,HarborCastOff, and theHarborWatersbreakdown. HarborMakeWay,HarborPontoon,HarborController: claims on an edge and docks moored from deep in the tree.HarborBuoy(aligned, anchored, modal),HarborAnchor,HarborSignals.showHarborSheet,HarborSheet(content-sized and draggable), breakwaters,showHarborDialog.HarborLighthouse,HarborBeacon,HarborLighthouseRegion: reveal, keep in sight, lift, and coverage.HarborScaleModel: a reference screen scaled to fit, with insets re-based.HarborChart,HarborChartOverlay, and theext.harbor.chartVM-service extension.package:harbor/testing.dart:pumpSeaTrial,HarborTrialDevicepresets andisInClearWater.