whats_new_kit_flutter 0.1.0
whats_new_kit_flutter: ^0.1.0 copied to clipboard
An Apple-style What's New sheet for Flutter — a faithful port of SvenTiigi/WhatsNewKit, themed from your app's ColorScheme.
whats_new_kit_flutter #
The polished "What's New in this version" screen that Apple shows after an update — for Flutter. A faithful port of SvenTiigi/WhatsNewKit, with a Flutter-shaped API and every colour taken from your app's own theme.
[Four What's New sheets presenting and dismissing]
What is this, in plain terms? #
When you ship an update, most users never find out what changed. The usual fix is a one-time screen listing the highlights — a title, a few rows of icon + headline + explanation, and a button to get on with it.
This package gives you that screen, and handles the fiddly part for you:
- Showing it exactly once. Not once per launch, not never — once per release, per user, and it survives restarts.
- Knowing when to show it. You declare what changed in each version; it works out which one the current user should see, if any.
- Looking right everywhere. Light and dark, phone and tablet, portrait and landscape, iOS and Android and desktop.
It has no required plugin dependencies, works on all six Flutter platforms, and compiles to WebAssembly.
If you have used the "What's New" screen in Apple's Calendar, Maps or Translate apps, this is that screen.
[Default dark sheet] [Calendar sheet] [Maps sheet] [Translate sheet with a two-tone title]
Quick start #
1. Add the dependency
flutter pub add whats_new_kit_flutter
2. Show a sheet
import 'package:flutter/material.dart';
import 'package:whats_new_kit_flutter/whats_new_kit_flutter.dart';
ElevatedButton(
onPressed: () => showWhatsNewSheet(
context,
version: '1.0.0',
title: "What's New",
features: <WhatsNewFeature>[
WhatsNewFeature(
icon: Icons.history,
title: 'Time Machine',
subtitle: 'Travel back in time.',
),
WhatsNewFeature(
icon: Icons.bolt,
title: 'Faster Everything',
subtitle: 'Twice the speed, half the battery.',
),
],
onContinue: () {},
),
child: const Text('Show me'),
);
That is the whole API for the common case. Colours come from
Theme.of(context), so it already matches your app. The returned Future
completes when the sheet is dismissed.
New to Flutter?
contextis the variable Flutter hands you inside abuildmethod or a callback. If you are insidebuild, just passcontext.
Contents #
| Section | Read this if you want to… |
|---|---|
| Show it once per release | stop repeating yourself on every launch |
| Telling it your app version | wire up where the version comes from |
| How it decides | understand the rules before trusting them |
| Theming | change colours and fonts |
| Responsive layout | phones, tablets, landscape, split view |
| Layout | change spacing, size and shape |
| Rich text | bold a word, add a link, colour part of the title |
| Actions | add a second button, a link, or haptics |
| Presentation | control sheet vs dialog vs full page |
| Storage | change or replace where "already seen" is recorded |
| Accessibility | what is handled for you, and what you control |
| Right-to-left | Arabic, Hebrew, Persian |
| Testing | write tests around it |
| Migrating from WhatsNewKit | you are coming from the Swift package |
| FAQ | something is not behaving |
Show it once per release #
The manual call above shows the sheet every time you call it. For the usual "show each release's notes once", declare your history and let the package decide.
final WhatsNewController controller = WhatsNewController(
versionStore: SharedPreferencesWhatsNewVersionStore(),
collection: <WhatsNew>[
WhatsNew.of(
version: '1.0.0',
title: 'Welcome',
features: <WhatsNewFeature>[
WhatsNewFeature(
icon: Icons.waving_hand_outlined,
title: 'Hello',
subtitle: 'Thanks for installing.',
),
],
),
WhatsNew.of(
version: '1.1.0',
title: "What's New in 1.1",
features: <WhatsNewFeature>[
WhatsNewFeature(
icon: Icons.speed,
title: 'Faster',
subtitle: 'Startup is twice as quick.',
),
],
),
],
);
MaterialApp(
home: WhatsNewScope(
controller: controller,
child: const WhatsNewAutoSheet(child: HomePage()),
),
);
On launch, WhatsNewAutoSheet reads the running app version, compares it
against what the user has already seen, and presents the right sheet — or
nothing at all. Dispose the controller when you are done with it, the same as
any ChangeNotifier.
Telling it your app version #
Reading the version is a policy decision — marketing version, build number, or a value from remote config — so this package does not take a plugin dependency to guess for you. Choose one:
// A. Read it from the bundle. Add package_info_plus to your own pubspec.
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
WhatsNewAppVersion.resolver =
() async => (await PackageInfo.fromPlatform()).version;
runApp(const MyApp());
}
// B. Set it directly, if you already know it.
WhatsNewAppVersion.overrideCurrent(const WhatsNewVersion(1, 2, 0));
// C. Pass it per call, and never configure anything.
showWhatsNewSheet(context, version: '1.2.0', /* … */);
WhatsNewController(currentVersion: const WhatsNewVersion(1, 2, 0), /* … */);
Forget to do any of these and you get a StateError naming all three options,
rather than a silently wrong 0.0.0.
Where does
WhatsNewAutoSheetgo? It presents through the nearestNavigator, so it must sit below one. Wrappinghomeis the simplest spot.MaterialApp.builderruns above the navigator it builds, so if you prefer that spot, giveMaterialAppanavigatorKeyand pass the same key toWhatsNewAutoSheet(navigatorKey: …).
How it decides what to show #
Ported from WhatsNewKit exactly:
- If the running version has already been shown → show nothing.
- Otherwise show the entry whose version matches it exactly.
- Failing that, fall back to the
major.minor.0entry — so declaring1.2.0also covers users on1.2.7— unless that entry has already been shown.
Two behaviours are inherited on purpose, and are worth knowing before you ship:
- Versions are compared for equality, never ordering. Someone upgrading
straight from
1.0.0to1.3.0sees the1.3.xsheet only, not every release they skipped. If you want a combined "everything since your last version" sheet, build that entry yourself and give it the new version number. - Dismissing counts as seen — including a swipe-down. Without this, anyone
who swipes the sheet away would be shown it again on every single launch. If
you would rather only count a deliberate tap, pass
markPresented: WhatsNewMarkPresented.primaryActionOnly.
Manual control #
WhatsNewController is a plain ChangeNotifier; you do not have to use the
automatic widget.
final WhatsNew? pending = await controller.resolvePending();
if (pending != null && context.mounted) {
await WhatsNewSheet.show(context, whatsNew: pending, versionStore: controller.versionStore);
}
await controller.markPresented(const WhatsNewVersion(1, 2, 0)); // suppress it
await controller.resetPresentedVersions(); // replay everything
Theming #
Nothing is hardcoded. Every colour is read from the ambient ColorScheme:
| What you see | Where it comes from |
|---|---|
| Title, feature titles | colorScheme.onSurface |
| Feature subtitles | colorScheme.onSurfaceVariant |
| Feature icons, secondary link | colorScheme.primary |
| Primary button | colorScheme.primary filled, colorScheme.onPrimary label |
| Sheet background, footer blur | colorScheme.surface |
[Dark] [Light]
Tint one sheet:
showWhatsNewSheet(context, accentColor: Colors.teal, /* … */);
Restyle every sheet in the app with a ThemeExtension:
ThemeData(
extensions: const <ThemeExtension<dynamic>>[
WhatsNewTheme(
accentColor: Color(0xFF32ADE6),
titleStyle: TextStyle(fontSize: 40, fontWeight: FontWeight.w800),
footerBlurSigma: 30,
pressedOpacity: 0.7,
),
],
)
Values resolve in this order — explicit argument → theme extension →
ColorScheme — so you can override as little or as much as you like.
Everything on WhatsNewTheme: accentColor, titleStyle,
featureTitleStyle, featureSubtitleStyle, primaryButtonTextStyle,
secondaryButtonTextStyle, primaryButtonBackgroundColor,
primaryButtonForegroundColor, sheetBackgroundColor, footerBlurSigma,
footerScrimColor, footerScrimOpacity, pressedOpacity, markdownStyle,
onMarkdownLinkTap, layout.
⚠️ Material 3 will change your seed colour.
ColorScheme.fromSeed(seedColor: Colors.blue)does not give you blue — it derives a tonal palette, which in dark mode is a pale lavender. That is Material behaving normally, not a bug in this package. If you want a literal accent, pin it:ColorScheme.fromSeed(seedColor: seed, brightness: brightness) .copyWith(primary: seed, onPrimary: Colors.white)The example app does exactly this, which is why it matches Apple's screenshots.
Layout #
Every measurement from the original is exposed on WhatsNewLayout, with the
same defaults:
showWhatsNewSheet(
context,
// …
layout: const WhatsNewLayout(
contentSpacing: 35, // title → feature list
featureListSpacing: 35, // between features
featureImageWidth: 56, // icon column
footerPrimaryButtonCornerRadius: 8,
footerBackground: WhatsNewFooterBackground.solid,
),
);
Every layout property and its default
| Property | Default | Controls |
|---|---|---|
showsScrollBar |
false |
scrollbar visibility |
scrollBottomContentInset |
150 |
empty space so content clears the footer |
contentSpacing |
60 |
gap between title and feature list |
contentPadding |
top: 65 |
padding around the content block |
contentHorizontalPadding |
16 |
horizontal padding of the content block |
featureListSpacing |
25 |
gap between features |
featureListPadding |
start: 15 |
padding around the feature list |
featureImageWidth |
40 |
width of the icon column |
featureImageSize |
28 |
rendered icon size |
featureHorizontalSpacing |
15 |
gap between icon and text |
featureCrossAxisAlignment |
center |
icon alignment against the text |
featureVerticalSpacing |
2 |
gap between title and subtitle |
footerActionSpacing |
15 |
gap between the two actions |
footerPrimaryButtonCornerRadius |
14 |
button corner radius |
footerPrimaryButtonVerticalPadding |
16 |
sets the button height (≈54) |
footerBlurBleed |
10 |
how far the blur extends above the footer |
footerBackground |
blur |
blur, solid or none |
bottomSafeAreaBehavior |
absorb |
whether the safe area is absorbed or added |
breakpoints |
WhatsNewBreakpoints() |
form-factor thresholds |
contentLayout |
adaptive |
one column, two columns, or automatic |
maxContentWidth |
560 |
caps and centres the content column |
twoColumnMinWidth |
600 |
when adaptive may split |
twoColumnMaxWidth |
1000 |
caps the split row on wide screens |
twoColumnSpacing |
48 |
gap between the columns |
twoColumnTitleFlex / twoColumnFeaturesFlex |
5 / 6 |
column width shares |
constrainedHorizontalMargin |
20 |
side margin once capped |
centerContentWhenConstrained |
true |
vertical centring on large surfaces |
sheetTopCornerRadius |
10 |
bottom sheet's top corners |
dialogMaxWidth / dialogMaxHeight |
520 / 720 |
dialog presentation size |
dialogInsetPadding |
24 |
dialog margin |
featuresPaddingResolver |
— | replaces the responsive feature padding |
footerPaddingResolver |
— | replaces the responsive footer padding |
Responsive layout #
The sheet adapts to the surface it is given, not to the device it is running on — so it behaves correctly in a split view, a resized desktop window, or a narrow dialog, not just on a whole screen.
Two arrangements #
One column — title above the features, actions pinned to the bottom. Used on phones in portrait, narrow split views, and tall tablets.
Two columns — title and actions on one side, the feature list scrolling on the other. Used when the surface is wide and shorter than it is tall. A landscape phone has plenty of width and almost no height; stacking a 34pt title, a feature list and a pinned footer would leave nothing to scroll in.
[Two-column layout on a landscape phone]
[Tablet portrait, content capped and centred] [Tablet landscape, two columns centred]
Choose explicitly if you prefer:
const WhatsNewLayout(
contentLayout: WhatsNewContentLayout.single, // never split
// or .twoColumn to always split, or .adaptive (the default)
twoColumnMinWidth: 600, // when adaptive may split
)
Line length is capped #
Left alone, a feature subtitle on a 1024pt tablet runs to a ~790pt line — far
past the point where text stops being readable. The content column is capped at
maxContentWidth (560 by default) and centred, and once capped it is also
centred vertically instead of clinging to the top of a tall screen.
const WhatsNewLayout(
maxContentWidth: 560, // double.infinity restores WhatsNewKit
twoColumnMaxWidth: 1000, // the same idea for the split layout
centerContentWhenConstrained: true,
)
Phones are never capped (393 < 560), so they keep WhatsNewKit's original geometry exactly.
Padding by form factor #
Inside the cap, padding still follows the size-class branches of the original:
| Surface | Feature list | Footer |
|---|---|---|
| Phone portrait | — | 20 / 20 / 80 |
| Phone landscape (height < 500) | — | 40 / 40 / 35 |
| Tablet (width ≥ 600) | 100 each side |
150 / 150 / 50 |
| Desktop (width ≥ 900) | 16 each side |
30 bottom |
Once maxContentWidth is capping the column those wide side insets would only
push it off-centre, so they collapse to constrainedHorizontalMargin while the
bottom inset is kept.
Move the thresholds, or replace the tables outright:
WhatsNewLayout(
breakpoints: const WhatsNewBreakpoints(
regularWidth: 700,
compactHeight: 450,
useShortestSide: true, // stricter parity with Apple's size classes
),
footerPaddingResolver: (WhatsNewFormFactor form) => switch (form) {
WhatsNewFormFactor.compact => const EdgeInsets.all(12),
_ => const EdgeInsets.all(40),
},
)
What is covered #
Twelve golden files and a responsive test suite pin the behaviour at phone portrait and landscape, tablet portrait and landscape, a narrow split view, a desktop window, and a 220×320 window — asserting the arrangement, the capping, and that nothing overflows at any of them.
Rich text #
Anywhere a title or subtitle is accepted, you can pass more than a plain string.
// Plain — what the shorthand constructors build for you
const WhatsNewText('Automatic Presentation')
// Inline Markdown: **bold**, *italic*, `code`, [links](https://example.com)
const WhatsNewText.markdown('Present it with `WhatsNewAutoSheet`, [docs](https://example.com).')
// Explicit spans, for a two-tone title
const WhatsNewText.rich(
TextSpan(children: <InlineSpan>[
TextSpan(text: "What's New\nin "),
TextSpan(text: 'Translate', style: TextStyle(color: Color(0xFF32ADE6))),
]),
semanticsLabel: "What's New in Translate", // what screen readers announce
)
// Built at layout time, when styling depends on context
WhatsNewText.builder((BuildContext context, TextStyle base) => TextSpan(/* … */))
Use WhatsNewFeature.rich when you want styled text or non-icon artwork:
const WhatsNewFeature.rich(
image: WhatsNewImage.asset('assets/sparkle.png'),
title: WhatsNewText('Automatic Presentation'),
subtitle: WhatsNewText.markdown('Declare a `WhatsNew` per version.'),
)
WhatsNewImage accepts .icon, .asset, or .widget for anything else — an
SVG, a Lottie animation, whatever you like.
Markdown links open in the browser by default. Route them elsewhere with
WhatsNewTheme(onMarkdownLinkTap: …), and restyle them with
InlineMarkdownStyle.
Actions #
showWhatsNewSheet(
context,
// …
continueLabel: 'Get Started',
onContinue: () => print('acknowledged'),
continueHaptic: const WhatsNewHaptic.notification(),
secondaryAction: WhatsNewSecondaryAction.openUrl(
title: 'Learn more',
url: Uri.parse('https://example.com/changelog'),
),
);
Secondary action variants:
| Constructor | Behaviour |
|---|---|
WhatsNewSecondaryAction.openUrl |
opens a URL in the browser |
WhatsNewSecondaryAction.dismiss |
closes the sheet |
WhatsNewSecondaryAction.present |
pushes another sheet on top |
WhatsNewSecondaryAction(onPressed: …) |
anything else |
The callback receives a WhatsNewActionContext that can dismiss() or
present() — the equivalent of the PresentationMode binding WhatsNewKit hands
to a custom action.
WhatsNewSecondaryAction(
title: const WhatsNewText('Skip setup'),
onPressed: (WhatsNewActionContext action) {
action.dismiss();
Navigator.of(action.context).pushNamed('/settings');
},
)
Haptics: WhatsNewHaptic.impact([style]), .selection(), .notification([kind]).
Presentation #
showWhatsNewSheet(context, presentation: WhatsNewPresentation.dialog, /* … */);
| Value | Result |
|---|---|
adaptive (default) |
bottom sheet on phone/tablet, dialog on wide desktop and web |
bottomSheet |
always a modal bottom sheet |
dialog |
always a centered dialog |
page |
pushes a full page onto the navigator |
To put the content inside a screen of your own — an onboarding flow, or a "What's New" row in Settings — use the widget directly:
Scaffold(body: WhatsNewView(whatsNew: myWhatsNew))
Storage #
WhatsNewVersionStore records which versions a user has seen.
| Implementation | Use when |
|---|---|
SharedPreferencesWhatsNewVersionStore |
the normal choice; survives restarts |
InMemoryWhatsNewVersionStore |
tests, or a demo that replays every launch |
CachingWhatsNewVersionStore |
wraps another store and preloads it, so decisions can be made while building a frame |
Keys are written as WhatsNewKit.<version> — byte-identical to WhatsNewKit's
UserDefaults format, so an app migrating from the Swift package keeps its
history with no migration code.
Back it with anything by extending the base class:
final class FirestoreVersionStore extends WhatsNewVersionStore {
@override
Future<List<WhatsNewVersion>> presentedVersions() async { /* … */ }
@override
Future<void> save(WhatsNewVersion version) async { /* … */ }
@override
Future<void> remove(WhatsNewVersion version) async { /* … */ }
@override
Future<void> removeAll() async { /* … */ }
}
Accessibility #
Handled for you, and covered by tests:
| Concern | What happens |
|---|---|
| Screen readers | Each feature reads as one element — "Showcase your new App Features. Present your new app features just like a native app." — rather than three separate fragments. |
| Headings | The title is exposed as a heading and names the route, so VoiceOver and TalkBack announce the sheet on open and can jump to it. |
| Buttons | Announced once, with the button trait, focusable, and activatable by a screen-reader double tap or the keyboard. A openUrl action also carries the link trait; an in-app action does not. |
| Tap targets | Every control is at least 48pt — clearing Apple's 44pt minimum, Material's 48dp, and WCAG 2.2 target size. A bare text link is padded out without changing how it looks. |
| Dynamic Type | Text scales with the system setting; tested to 300% in portrait and landscape. |
| Bold Text | MediaQuery.boldText thickens every weight. Flutter surfaces the setting but leaves honouring it to the app; this package honours it. |
| Reduce Transparency / high contrast | The footer blur is replaced with an opaque fill. |
| Contrast | The package's defaults pass textContrastGuideline in both light and dark. |
The suite asserts all four of Flutter's official guidelines —
textContrastGuideline, iOSTapTargetGuideline, androidTapTargetGuideline
and labeledTapTargetGuideline — plus the semantics tree itself.
const WhatsNewLayout(
minTapTargetSize: 48, // raise it further if you like
respectHighContrast: true, // false keeps the blur regardless
)
One contrast caveat worth knowing #
White on Apple's system blue is 3.65:1 in dark and 4.02:1 in light.
WCAG AA asks 4.5:1 for normal text but only 3:1 for large or bold text, and the
button label is 17pt semibold, which qualifies — so it passes. Flutter's
textContrastGuideline ignores font weight and applies the stricter bar, which
is why the example app's Apple-pinned palette is exempted from that one check
and the package's own defaults are tested instead.
If you want to clear 4.5:1 outright, darken the button or let Material pick the pair for you:
// Material guarantees a contrast-correct primary/onPrimary pair.
ColorScheme.fromSeed(seedColor: Colors.blue, brightness: brightness)
Right-to-left #
Arabic, Hebrew and Persian are supported without configuration. The feature icon moves to the trailing edge, text hugs the right, the two-column layout mirrors, and the centred title stays centred.
[Arabic in one column] [Arabic in two columns]
All insets are directional (EdgeInsetsDirectional) or symmetric, so nothing
has to be flipped by hand. If you supply your own inset resolvers, use
EdgeInsetsDirectional to keep that property.
Testing your integration #
Keep the app version and the store deterministic, and nothing touches a plugin:
testWidgets('shows the 1.1 sheet on a fresh install', (WidgetTester tester) async {
final WhatsNewController controller = WhatsNewController(
currentVersion: const WhatsNewVersion(1, 1, 0), // no package_info_plus
versionStore: InMemoryWhatsNewVersionStore(), // no shared_preferences
collection: myReleaseNotes,
);
await controller.load();
expect(controller.pendingWhatsNew?.version, const WhatsNewVersion(1, 1, 0));
});
WhatsNewAppVersion.overrideCurrent(…) does the same globally, if you would
rather set it once in main.
Migrating from WhatsNewKit #
| Swift | Dart |
|---|---|
WhatsNew(version:title:features:) |
WhatsNew.of(version:title:features:) |
WhatsNew.Feature(image:title:subtitle:) |
WhatsNewFeature(icon:title:subtitle:) |
WhatsNew.Layout |
WhatsNewLayout |
WhatsNewEnvironment |
WhatsNewController |
.environment(\.whatsNew, …) |
WhatsNewScope |
.whatsNewSheet() |
WhatsNewAutoSheet |
.sheet(whatsNew:) |
WhatsNewSheet.show |
UserDefaultsWhatsNewVersionStore |
SharedPreferencesWhatsNewVersionStore |
@WhatsNewCollectionBuilder |
a plain List<WhatsNew> |
WhatsNewViewController |
WhatsNewView |
Deliberate differences, and why
- Version parsing is positional. WhatsNewKit drops a non-numeric component
and shifts the rest left, so
"1.x.3"becomes1.3.0; here it becomes1.0.3. A+buildor-prereleasesuffix is stripped first, because pubspec versions look like1.2.3+45.WhatsNewVersion.parseCompatreproduces the Swift behaviour exactly if you need it. Title.foregroundColoris gone. The Swift view never reads it — it is dead code. Colour the title throughWhatsNewTheme.titleStyleor aWhatsNewText.richspan.- The default layout is immutable. WhatsNewKit's is a mutable static, which in Dart would break hot reload and parallel tests.
PrimaryAction.onDismissisonPressed. It only fires on the button, so the original name was misleading. The any-dismissal hook is the top-levelonDismissargument.- The button label defaults to
onPrimary, not white, which stays readable on a light accent colour. - The footer blur is drawn on every platform. WhatsNewKit's is iOS-only.
- Content is capped and can split into two columns. WhatsNewKit only ever
pads a full-width single column, which reads poorly on a tablet and wastes a
landscape phone. Set
maxContentWidth: double.infinityandcontentLayout: WhatsNewContentLayout.singlefor the original behaviour. - No iCloud key-value store. Implement
WhatsNewVersionStoreover a platform channel if you need cross-device sync. - Haptics carry no intensity, and notification feedback maps to a medium
impact — Flutter's
HapticFeedbackexposes nothing closer.
FAQ #
The sheet shows every time I launch the app.
You are almost certainly not passing a versionStore. Without one, nothing is
recorded and every call presents. Pass
SharedPreferencesWhatsNewVersionStore() to showWhatsNewSheet, or use a
WhatsNewController, which creates one for you.
Also check you are not using InMemoryWhatsNewVersionStore — it forgets
everything when the process exits, which is what the example app wants but
probably not what you want.
The sheet never shows.
Three usual causes:
- The version was already recorded. Call
controller.resetPresentedVersions()to replay it during development. - No entry matches the running version. Remember the fallback is only ever
major.minor.0— an entry for1.0.0does not cover a user on1.2.0. Checkcontroller.currentVersionandcontroller.presentedVersions. WhatsNewAutoSheetsits above theNavigator. See the note in Show it once per release.
My accent colour comes out wrong.
Material 3 remaps seed colours. See the warning in Theming.
What does it depend on?
Two first-party packages, and neither is required:
| Package | Used for | How to avoid it |
|---|---|---|
shared_preferences |
the default persistent store | supply your own WhatsNewVersionStore |
url_launcher |
WhatsNewSecondaryAction.openUrl and Markdown links |
use a plain onPressed callback, and set WhatsNewTheme.onMarkdownLinkTap |
There is deliberately no package_info_plus dependency — see
Telling it your app version. That keeps the
package WASM-compatible and free of platform channels it does not need.
Does it work on Android / web / desktop?
Yes — it is pure Flutter with no platform code. The visual reference is iOS
because that is what it reproduces, but nothing is iOS-only. On a wide desktop
window the default adaptive presentation switches to a dialog.
How do I show a combined "everything you missed" sheet?
Build that entry yourself and give it the current version number. The package deliberately does not merge entries, because merged copy usually needs rewriting rather than concatenating.
Example #
git clone https://github.com/sudhi001/whats_new_kit_flutter
cd whats_new_kit_flutter/example
flutter run
The example reproduces WhatsNewKit's own sheets (WhatsNewKit, Calendar, Maps, Translate), plus an automatic-presentation demo with a version picker and store inspector, and a layout playground wired to every geometry constant.
Compatibility #
- Dart
^3.6.0, Flutter>=3.27.0 - iOS, Android, macOS, Windows, Linux, Web — including
dart2wasm - Scores 160/160 on pub.dev's analysis
Contributing #
Issues and pull requests are welcome. Before opening a PR:
dart format .
flutter analyze # must be clean
flutter test # must be green, goldens included
Layout changes should keep the golden tests honest — regenerate with
flutter test --update-goldens and review the diff rather than accepting it
blindly.
Credits #
All credit for the original design and API to Sven Tiigi and WhatsNewKit.
License #
MIT — see LICENSE.