
Small details that build taste in Flutter.
curated by Kamran Bekirov

curated by Kamran Bekirov
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.
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.',
],
},
};
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.
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.
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.
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