Skip to main content

app_changelog 1.2.0 copy "app_changelog: ^1.2.0" to clipboard
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 #

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, so didUpdate is false and the notes for the version the user just installed aren't shown.
  • didUpdate stays true until you call markVersionSeen(). 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+41 isn't an update from 2.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 by app_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, or 1.2.3+42.
  • Platform identifiers start with a letter and contain letters, digits, _, or -. They're case-insensitive: @iOS and @ios are the same platform.
  • ### Title directly 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 as YYYY-MM-DD: ## 4.0.0 - 2024-01-31 or ## @ios: 2.11.1 - 2024-02-03. It's available as date, a DateTime at 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:

  1. includes every untagged change,
  2. includes changes tagged for that platform,
  3. excludes changes tagged for other platforms,
  4. preserves the original order, and
  5. 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}');
}
  • find returns null when the platform has no release with that version. Build metadata is ignored, so find(version: '2.9.0+120') matches 2.9.0.
  • latestFor and latestRelease return the highest version by semantic versioning, regardless of where the release appears in the file. Pass includePreReleases: false to skip versions such as 3.0.0-beta.1.
  • iOS and Android histories are independent. With ## @ios: 2.11.1 and ## @android: 2.10.1, the latest iOS release is 2.11.1 and the latest Android release is 2.10.1.
  • In a Flutter app, path is an asset key and must be listed under flutter: 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=version lines, 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.1 are skipped, since store build names must be MAJOR.MINOR.PATCH. Pass --pre-releases to 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.1 whose only note is - @android: .... The fix is usually the heading ## @android: 1.2.1, and the message says so, or
  • pubspec.yaml is at a newer version than any release in the changelog, which usually means the release notes were forgotten. A pubspec.yaml behind the changelog is fine, since --build-name sets 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.0 aren'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.9 are rejected. Build metadata (+42) is ignored when comparing versions, so 1.0.0+1 and 1.0.0+2 count 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_changelog commands, 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.

0
likes
150
points
157
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

One changelog. Every platform. A platform-aware CHANGELOG.md parser for shared and platform-specific Flutter app release notes.

Repository (GitHub)
View/report issues

Topics

#changelog #release-notes #versioning #ci

License

MIT (license)

Dependencies

flutter, package_info_plus, shared_preferences

More

Packages that depend on app_changelog