app_changelog 1.2.0
app_changelog: ^1.2.0 copied to clipboard
One changelog. Every platform. A platform-aware CHANGELOG.md parser for shared and platform-specific Flutter app release notes.
app_changelog #
One changelog. Every platform.
app_changelog is a lightweight, platform-aware changelog parser for app
release notes. Keep the notes for iOS, Android, and any other platform in one
CHANGELOG.md. It handles:
- shared releases that apply to every platform,
- platform-specific releases, such as an iOS-only hotfix, and
- platform-specific changes inside a shared release.
It's built for Flutter apps. It reads the changelog asset, the installed version and the current platform for you, so showing "What's new" takes one call. The same changelog can also drive your CI/CD pipeline.
## 2.9.0
### What's new
- @ios: iPad pencil support
- A brand-new welcome screen
- @android: Accessibility improvements
- Bug fixes and improvements
## @ios: 2.11.1
- Bug fixes and improvements
// On Android, with version 2.9.0 installed:
final release = await AppChangelog.currentRelease();
print(release?.title); // What's new
print(release?.notes);
// [A brand-new welcome screen, Accessibility improvements, Bug fixes and improvements]
Contents #
- Installation
- Quick start
- Tracking updates
- Markdown format
- Querying a changelog
- CI/CD integration
- API overview
- Errors
- Limitations
Installation #
flutter pub add app_changelog
That's the only dependency you need. app_changelog uses
package_info_plus internally
to read the installed version, and
shared_preferences to
remember the last version a user has seen, so you don't have to add either
yourself.
Quick start #
1. Bundle CHANGELOG.md as an asset in pubspec.yaml:
flutter:
assets:
- CHANGELOG.md
2. Load the notes for the installed version:
import 'package:app_changelog/app_changelog.dart';
final release = await AppChangelog.currentRelease();
AppChangelog.currentRelease() reads CHANGELOG.md, the installed version (with
package_info_plus) and the current platform, and returns that version's
notes filtered for the platform. It returns null if the changelog has no
release for the installed version on this platform.
If your changelog lives somewhere else, pass its asset path:
final release = await AppChangelog.currentRelease(
path: 'assets/release_notes.md',
);
3. Display the title and notes:
Future<void> showWhatsNew(BuildContext context) async {
final release = await AppChangelog.currentRelease();
if (release == null || release.isEmpty || !context.mounted) return;
await showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: Text(release.title),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [for (final note in release.notes) Text('• $note')],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('OK'),
),
],
),
);
}
4. Show them once per version. Call AppChangelog.track() before
runApp, then show the notes when the user has updated:
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await AppChangelog.track();
runApp(const MyApp());
}
if (AppChangelog.didUpdate) {
await showWhatsNew(context);
await AppChangelog.markVersionSeen();
}
See Tracking updates for the details.
Tracking updates #
AppChangelog.track() remembers the last version whose notes the user has
seen, so your app can tell a new install from an update. Call it once, before
runApp:
await AppChangelog.track();
AppChangelog.isNewInstall; // true on the first launch after installing
AppChangelog.didUpdate; // true when the user has updated since the
// last version marked as seen
AppChangelog.previousVersion; // the last version marked as seen, if any
await AppChangelog.markVersionSeen(); // didUpdate is false until the next update
- On a new install,
track()stores the installed version straight away, sodidUpdateisfalseand the notes for the version the user just installed aren't shown. didUpdatestaystrueuntil you callmarkVersionSeen(). If your app can't show the notes on one launch, for example because the user isn't signed in, it can show them on the next.markVersionSeen()never stores a lower version, so downgrades don't bring old notes back.- Build metadata is ignored, so
2.9.1+41isn't an update from2.9.1+40. - iOS and Android version histories are tracked separately, since each device stores its own installed version.
Showing every release since the last update #
A user who jumps from 2.7.0 to 2.9.1 might have missed the notes for
2.8.0 and 2.9.0. unseenReleases() returns every release on the current
platform above the last version marked as seen, up to the installed version,
newest first:
final List<PlatformRelease> releases = await AppChangelog.unseenReleases();
for (final release in releases) {
print('${release.version}: ${release.notes}');
}
Releases with no notes for the platform are skipped, and so are
pre-releases unless the installed version is itself a pre-release. The list
is empty when didUpdate is false.
Adding tracking to a published app #
Existing users have nothing stored yet, so by default they look like new
installs. Pass isExistingInstall to tell them apart, using something your
app already stores:
await AppChangelog.track(
isExistingInstall: () async => prefs.getBool('onboarding_done') ?? false,
);
When it returns true, didUpdate is true and previousVersion is null.
unseenReleases() then returns only the installed version's notes, not the
whole history.
Storage #
The version is stored under the key app_changelog.seen_version with
shared_preferences. Pass key to change it, or read and write to use
your own storage instead:
final Box<String> box = await Hive.openBox<String>('settings');
await AppChangelog.track(
read: (key) async => box.get(key),
write: (key, value) => box.put(key, value),
);
In widget tests, set the stored version with
SharedPreferences.setMockInitialValues before calling track().
Markdown format #
Note:
@ios:,@android:and other@platform:prefixes are conventions defined byapp_changelog. They aren't part of Markdown, and other Markdown renderers (GitHub, pub.dev) show them as plain text.
# Changelog
Anything before the first release heading is ignored.
## 4.0.0
- Major fixes
- Bug fixes and improvements
## 3.12.1
### What's new
- Fix firebase
- Bug fixes and improvements
## 2.9.0
### What's new
- @ios: iPad pencil support
- A brand-new welcome screen
- A new language picker with search
- Improved translations
- @android: Accessibility improvements
- Bug fixes and improvements
## @ios: 2.11.1
- Bug fixes and improvements
## @android: 2.10.1
- Bug fixes and improvements
Release headings #
| Heading | Applies to |
|---|---|
## 4.0.0 |
Every platform |
## @ios: 2.11.1 |
iOS only |
## @android: 2.10.1 |
Android only |
## @web: 1.0.0 |
Any custom platform, e.g. web |
## 4.0.0 - 2024-01-31 |
Every platform, released on 31 January 2024 |
- Versions must follow semantic versioning, such as
1.2.3,2.0.0-beta.1, or1.2.3+42. - Platform identifiers start with a letter and contain letters, digits,
_, or-. They're case-insensitive:@iOSand@iosare the same platform. ### Titledirectly under a release heading sets an optional title. Without one, the title is"What's new".- An optional release date follows the version after
-, written asYYYY-MM-DD:## 4.0.0 - 2024-01-31or## @ios: 2.11.1 - 2024-02-03. It's available asdate, aDateTimeat midnight UTC.
Shared notes across platform versions #
When several platforms ship the same notes under different version numbers, stack their headings. Platform-specific headings with nothing but blank lines between them share the title and changes below them:
## @android: 1.2.1 - 2024-01-31
## @ios: 1.0.1
- iPad updates
- @android: Some Google stuff
- @ios: Some iOS stuff
Android 1.2.1 shows "iPad updates" and "Some Google stuff", and iOS 1.0.1
shows "iPad updates" and "Some iOS stuff". Each heading is its own release, so
each can have its own date, and changes can target any platform in the stack.
Shared headings (## 1.2.1) never stack: a shared heading with no changes is
an empty release.
Changes #
Each change is a list item (-, *, or +):
| Change | Shown on |
|---|---|
- Bug fixes and improvements |
Every platform the release applies to |
- @ios: iPad pencil support |
iOS only |
- @android: Accessibility improvements |
Android only |
When you request a release for a platform, app_changelog:
- includes every untagged change,
- includes changes tagged for that platform,
- excludes changes tagged for other platforms,
- preserves the original order, and
- strips the
@platform:prefix from the displayed text.
A change in a platform-specific release can only target that release's
platform. For example, - @android: ... under ## @ios: 2.11.1 is reported
as an error.
You can wrap a long change onto indented continuation lines, which are joined
with a single space. To start a change with a literal @ followed by a
colon, escape it as \@, for example - \@team: thanks for the reports. An
@ that isn't followed by an identifier and a colon, as in
- @mentions are now supported, needs no escaping.
Blank lines and single-line HTML comments (<!-- ... -->) are ignored.
Querying a changelog #
Static helpers #
Each helper loads the changelog (CHANGELOG.md, or path:) and answers one
question:
// The installed version's notes on this platform. Flutter only.
final current = await AppChangelog.currentRelease();
// The newest release on this platform, or on the one you name.
final latest = await AppChangelog.latestRelease();
final latestAndroid = await AppChangelog.latestRelease(platform: 'android');
// The running platform: ios, android, web, macos, windows or linux.
print(AppChangelog.currentPlatform);
Loading the whole changelog #
For anything else, load the changelog once and query it. Every query returns notes already filtered for the platform.
final changelog = await AppChangelog.load(); // or load(path: '...')
// Notes for a specific version.
final release = changelog.find(platform: 'ios', version: '2.9.0');
print(release?.title); // What's new
print(release?.version); // 2.9.0
print(release?.notes); // [iPad pencil support, A brand-new welcome screen, ...]
// Latest release for a platform.
final latest = changelog.latestFor('ios');
// Every release that applies to Android, newest first.
for (final release in changelog.releasesFor('android')) {
print('${release.version}: ${release.notes}');
}
findreturnsnullwhen the platform has no release with that version. Build metadata is ignored, sofind(version: '2.9.0+120')matches2.9.0.latestForandlatestReleasereturn the highest version by semantic versioning, regardless of where the release appears in the file. PassincludePreReleases: falseto skip versions such as3.0.0-beta.1.- iOS and Android histories are independent. With
## @ios: 2.11.1and## @android: 2.10.1, the latest iOS release is2.11.1and the latest Android release is2.10.1. - In a Flutter app,
pathis an asset key and must be listed underflutter: assets:. In a Dart script, it's a file path relative to the working directory.
If you already have the Markdown as a string, for example in a test, parse it
with AppChangelog.parse(markdown).
CI/CD integration #
app_changelog comes with a command line, so the version in your changelog
heading can be the version you ship. Run it from your Flutter project with
dart run.
Build names from the changelog #
version prints the newest version, ready for Flutter's --build-name:
$ dart run app_changelog version --platform ios
2.11.1
flutter build ipa --build-name=$(dart run app_changelog version --platform ios)
flutter build appbundle --build-name=$(dart run app_changelog version --platform android)
--build-name sets Android's versionName and iOS's FLUTTER_BUILD_NAME,
which the Flutter template's Info.plist uses as the app version. Build
numbers stay the responsibility of your CI/CD platform, so build metadata
(+42) is dropped.
-
Without
--platform, it reads the platform folders in your project (android,ios,web, ...) and prints their version if they all agree. If a platform-specific release gives them different versions, it fails and asks you to pass--platform. -
With several platforms, it prints
platform=versionlines, which you can append to$GITHUB_OUTPUT:$ dart run app_changelog version --platform ios,android ios=2.11.1 android=2.9.0 -
Pre-releases such as
2.0.0-beta.1are skipped, since store build names must beMAJOR.MINOR.PATCH. Pass--pre-releasesto include them.
GitHub Actions:
- name: Read build names from CHANGELOG.md
id: versions
run: dart run app_changelog version --platform ios,android >> "$GITHUB_OUTPUT"
- name: Build iOS
run: >
flutter build ipa
--build-name=${{ steps.versions.outputs.ios }}
--build-number=${{ github.run_number }}
- name: Build Android
run: >
flutter build appbundle
--build-name=${{ steps.versions.outputs.android }}
--build-number=${{ github.run_number }}
Any shell-based CI (Codemagic, Bitrise, GitLab CI, ...):
BUILD_NAME="$(dart run app_changelog version --platform android)"
flutter build appbundle --build-name="$BUILD_NAME" --build-number="$BUILD_NUMBER"
Checking the changelog before a release #
check fails the build before anything ships with the wrong notes:
$ dart run app_changelog check
android 2.9.0 (3 notes)
ios 2.11.1 (1 note)
CHANGELOG.md is ready to release.
It fails when:
- the changelog can't be parsed (the error names the line),
- a platform has no release,
- a platform's newest release has no notes for it, such as a shared
## 1.2.1whose only note is- @android: .... The fix is usually the heading## @android: 1.2.1, and the message says so, or pubspec.yamlis at a newer version than any release in the changelog, which usually means the release notes were forgotten. Apubspec.yamlbehind the changelog is fine, since--build-namesets the version.
It also warns, without failing, when ios/Runner/Info.plist or
macos/Runner/Info.plist doesn't read its version from
$(FLUTTER_BUILD_NAME). In that case, --build-name won't change the version
on that platform.
Options #
| Option | Description |
|---|---|
--platform <name> |
A platform to read. Repeat it, or separate names with commas. Defaults to the project's platform folders. |
--path <file> |
The changelog to read. Defaults to CHANGELOG.md. |
--pre-releases |
Include pre-releases. |
Both commands exit with 0 on success, 1 when a check fails or a platform
has no release, 64 for invalid arguments, 65 for a malformed changelog,
and 66 when the changelog file is missing.
From your own scripts #
Scripts run with dart run can also use the library directly, where
AppChangelog.load reads from the file system instead of the asset bundle:
import 'package:app_changelog/app_changelog.dart';
Future<void> main() async {
final latest = await AppChangelog.latestRelease(
platform: 'ios',
includePreReleases: false,
);
print(latest?.version);
}
API overview #
Everything is available from package:app_changelog/app_changelog.dart, in
Flutter apps and in scripts run with dart run.
Static helpers. path defaults to CHANGELOG.md.
| API | Description |
|---|---|
AppChangelog.load({path}) |
Loads and parses the changelog. |
AppChangelog.parse(String) |
Parses and validates a Markdown string. |
AppChangelog.currentRelease({path}) |
The installed version's notes on this platform, or null. Flutter only. |
AppChangelog.latestRelease({platform, path, includePreReleases}) |
The newest release on platform (default: this one), or null. |
AppChangelog.currentPlatform |
The running platform's identifier, such as ios. |
Tracking updates. Call track() first. Flutter only.
| API | Description |
|---|---|
AppChangelog.track({key, read, write, isExistingInstall}) |
Reads the last version marked as seen, and records new installs. |
AppChangelog.isNewInstall |
Whether this is the first launch since the app was installed. |
AppChangelog.didUpdate |
Whether the installed version is newer than the last version marked as seen. |
AppChangelog.previousVersion |
The last version marked as seen before this launch, or null. |
AppChangelog.markVersionSeen() |
Stores the installed version as seen. |
AppChangelog.unseenReleases({path}) |
Releases since the last version marked as seen, newest first. |
Command line. Run from your project with dart run. See
CI/CD integration.
| Command | Description |
|---|---|
dart run app_changelog version |
Prints the newest version, for flutter build --build-name. |
dart run app_changelog check |
Fails if the changelog isn't ready to release. |
Queries on a loaded changelog.
| API | Description |
|---|---|
changelog.find(platform:, version:) |
The release at that version, filtered for the platform, or null. |
changelog.latestFor(platform, {includePreReleases}) |
The highest-version release for the platform, or null. |
changelog.releasesFor(platform) |
All releases for the platform, filtered, newest first. |
changelog.platforms |
Platforms named explicitly in the changelog. |
PlatformRelease is a release filtered for one platform, as returned by
every query:
| Member | Description |
|---|---|
platform |
The platform the notes are for, such as ios. |
version |
The release's version on that platform (AppVersion). |
title |
The release title, "What's new" by default. |
date |
The release date on that platform (DateTime?). |
changes |
The filtered List<ChangelogChange>, in source order. |
notes |
The changes' display text as a List<String>. |
isEmpty |
Whether the platform has no changes to show. |
ChangelogChange has the display text and the targeted platform
(null when shared). Its toString() returns the text, so a list of changes
prints readably.
AppVersion is a semantic version with major, minor, patch,
preRelease, build, and isPreRelease. It's Comparable and supports <,
<=, >, and >=.
All models are immutable. Their lists and maps are unmodifiable, and they implement value equality.
Errors #
Malformed changelogs throw a ChangelogFormatException (a
FormatException) that describes the problem and includes the 1-based line
number where it was found:
ChangelogFormatException: Line 12: Invalid version "2.9" in release heading "## 2.9". Versions must follow semantic versioning, for example "1.2.3" or "2.0.0-beta.1".
ChangelogFormatException: Line 20: The change "@android: Wrong" targets "android", but release "@ios: 2.11.1" only applies to ios. Remove the "@" prefix or move the change to a release for "android".
ChangelogFormatException: Line 31: Ambiguous release 2.9.0: a shared release and a platform-specific release cannot have the same version. ...
Validation covers:
- invalid versions and platform identifiers,
- invalid release dates,
- changes that target a platform outside their release,
- a platform repeated in stacked headings,
- empty changes and empty titles,
- duplicate releases for the same platform and version,
- a shared release and a platform-specific release with the same version, and
- unexpected Markdown content.
Query methods throw an ArgumentError for an invalid platform identifier, and
find throws a FormatException for an invalid version. Using
isNewInstall, didUpdate, previousVersion, markVersionSeen or
unseenReleases before AppChangelog.track() throws a StateError.
Limitations #
- One title per release. Keep a Changelog–style sections (
### Added,### Fixed) aren't supported, and nested lists are rejected. - Strict headings.
## Unreleased,## [1.2.0] - 2024-01-01, and## v1.2.0aren't valid release headings. Headings must be exactly## <version>or## @platform: <version>, optionally followed by- YYYY-MM-DD. - One platform per change or heading. A release is either shared or for a single platform. To give several, but not all, platforms the same notes, stack their headings.
- Plain text. Change text is returned exactly as written. Inline Markdown
such as
**bold**or links isn't rendered or stripped. - Semantic versions only. Two-part versions such as
2.9are rejected. Build metadata (+42) is ignored when comparing versions, so1.0.0+1and1.0.0+2count as the same release. - Flutter projects only. The package depends on the Flutter SDK, so a
pure Dart project (a server or a standalone CLI) can't add it. Scripts
inside a Flutter project, and the
dart run app_changelogcommands, still work. - No UI. The package returns notes but doesn't display them. See Quick start for a dialog you can adapt.
License #
MIT. See LICENSE.