Flutter Pro Design

Small details that build taste in Flutter.

curated by Kamran BekirovKamran Bekirov

Show users what changed when they update

You update something in your Flutter app. Maybe add a new feature or fix an issue users were facing. Then write a changelog and send update to the stores. When users open your app after an update -whether after updating from store manually or automatically in background- . You can show inform them when they open your app after updating and it has some subtle details.

The data

Keep changelogs as a versioned map in Dart:

const Map<String, Map<Language, List<String>>> _changelog = {
  '1.3.0': {
    Language.en: [
      'export transactions to CSV.',
    ],
  },
  '1.2.0': {
    Language.en: [
      'reorder categories with drag.',
      'unlimited accounts.',
    ],
  },
};
  • You can also fetch them from a remote source. Sometimes it's better so you can update errors or add missed things.

Make sure you add it in all languages your app supports. Group by language now so locale support is a one-line change later. Instead of using 'en' as keys, consider using an enum so you don't make a typo.

Tracking what the user has seen

The example is shown using Cubit, you can use any state management approach you wish.

Use package_info_plus to read the current version and SharedPreferences to persist the last version the user acknowledged. Store _currentVersion as a field — you'll need it later in dismiss().

class ChangelogCubit extends Cubit<List<String>?> {
  String? _currentVersion;
  
  ChangelogCubit() : super(null);
  
  Future<void> initialize({required Language language}) async {
    final String current = (await PackageInfo.fromPlatform()).version;
    _currentVersion = current;
  
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    final String? lastSeen = prefs.getString('last_seen_version');
  
    // First install — nothing to show, save and move on
    if (lastSeen == null) {
      await prefs.setString('last_seen_version', current);
      return;
    }
  
    // Already up to date
    if (lastSeen == current) return;
  
    final List<String> changes = unseenChanges(
      lastSeen: lastSeen,
      current: current,
      language: language,
    );
  
    if (changes.isEmpty) {
      await prefs.setString('last_seen_version', current);
      return;
    }
  
    emit(changes);
  }
}

Two details matter here.

Don't show on first install. When lastSeen is null, the user has never opened the app before. Showing a changelog on fresh install is confusing — they haven't used the old version. Save the current version and skip.

Show the full range, not just the latest

If a user skips from 1.2.0 to 1.5.0 without opening the app in between, they've missed 1.3.0 and 1.4.0 too. Collect everything in the gap:

List<String> unseenChanges({
  required String lastSeen,
  required String current,
  required Language language,
}) {
  final Version lastSeenVersion = Version.parse(lastSeen);
  final Version currentVersion = Version.parse(current);
  
  final List<String> result = [];
  
  for (final entry in _changelog.entries) {
    final Version entryVersion = Version.parse(entry.key);
  
    if (entryVersion > lastSeenVersion && entryVersion <= currentVersion) {
      final changes = entry.value[language];
      if (changes != null) result.addAll(changes);
    }
  }
  
  return result;
}

Use pub_semver for the comparison — it's already a transitive dependency of the Flutter tooling and handles edge cases your manual string split won't.

Dismiss saves the version

Don't save the version until the user dismisses. If the app is force-closed before they see the changelog, it shows again next launch. That's correct behavior.

Future<void> dismiss() async {
  final String? version = _currentVersion;
  if (version != null) {
    final SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setString('last_seen_version', version);
  }
  
  emit(null);
}

Only at this point does the version get saved. Next launch, lastSeen == current, nothing shows.

It's ready. Initialize it on app open, show it right away on the root page of your app. You can also be creative and show a button with sparks, or display them right inside page if your content let's too. For example in example image you can see I morph bottom bar to show changelog in expen.app.

The state is just List<String>?. Null means nothing to show. Non-null means show it. Your UI reacts to that and transitions naturally.

// Cubit state: List<String>?
// null     → show normal UI
// [...]    → morph into changelog