Auto Updates
Keep your app always up-to-date with automatic update checks and notifications
Overview
MiyoList includes a built-in update system that automatically checks for new versions on GitHub releases. Users are notified when updates are available and can download and install them with a single click.
✓ Key Features: Automatic version checking, update notifications, changelog display, one-click updates, background downloads
How It Works
Version Check
The app periodically checks GitHub releases API for the latest version:
// lib/core/services/update_service.dart
class UpdateService {
final Dio _dio;
final PackageInfo _packageInfo;
Future<Release?> checkForUpdates() async {
try {
// Get latest release from GitHub
final response = await _dio.get(
'https://api.github.com/repos/Baconana-chan/miyolist/releases/latest',
);
final release = Release.fromJson(response.data);
final currentVersion = Version.parse(_packageInfo.version);
final latestVersion = Version.parse(
release.tagName.replaceFirst('v', ''),
);
// Compare versions
if (latestVersion > currentVersion) {
return release;
}
return null;
} catch (e) {
debugPrint('Update check failed: $e');
return null;
}
}
}
class Release {
final String tagName;
final String name;
final String body;
final List<Asset> assets;
final DateTime publishedAt;
Release({
required this.tagName,
required this.name,
required this.body,
required this.assets,
required this.publishedAt,
});
factory Release.fromJson(Map<String, dynamic> json) {
return Release(
tagName: json['tag_name'],
name: json['name'],
body: json['body'],
assets: (json['assets'] as List)
.map((a) => Asset.fromJson(a))
.toList(),
publishedAt: DateTime.parse(json['published_at']),
);
}
} Notification
When an update is available, show a notification with changelog:
// lib/features/updates/presentation/widgets/update_dialog.dart
class UpdateDialog extends StatelessWidget {
final Release release;
const UpdateDialog({required this.release});
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Row(
children: [
Icon(Icons.system_update, color: Colors.orange),
SizedBox(width: 8),
Text('Update Available'),
],
),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Version ${release.tagName}',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text(
release.name,
style: TextStyle(fontSize: 16),
),
SizedBox(height: 16),
Text(
'What\'s New:',
style: TextStyle(fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
MarkdownBody(
data: release.body,
styleSheet: MarkdownStyleSheet.fromTheme(
Theme.of(context),
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('Later'),
),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
_downloadUpdate(context, release);
},
child: Text('Update Now'),
),
],
);
}
} Download
Download the appropriate installer for the platform:
// lib/core/services/download_service.dart
class DownloadService {
final Dio _dio;
Future<String> downloadUpdate(
Release release,
void Function(double) onProgress,
) async {
// Find appropriate asset for platform
final asset = _findAssetForPlatform(release.assets);
if (asset == null) {
throw Exception('No installer found for this platform');
}
// Get download directory
final downloadDir = await getDownloadsDirectory();
final filePath = '${downloadDir.path}/${asset.name}';
// Download with progress
await _dio.download(
asset.downloadUrl,
filePath,
onReceiveProgress: (received, total) {
if (total != -1) {
onProgress(received / total);
}
},
);
return filePath;
}
Asset? _findAssetForPlatform(List<Asset> assets) {
if (Platform.isWindows) {
return assets.firstWhere(
(a) => a.name.endsWith('.exe') || a.name.endsWith('.msi'),
orElse: () => null,
);
} else if (Platform.isLinux) {
return assets.firstWhere(
(a) => a.name.endsWith('.AppImage') || a.name.endsWith('.deb'),
orElse: () => null,
);
} else if (Platform.isAndroid) {
return assets.firstWhere(
(a) => a.name.endsWith('.apk'),
orElse: () => null,
);
}
return null;
}
} Installation
Launch the installer or prompt user to install:
// lib/core/services/installer_service.dart
class InstallerService {
Future<void> installUpdate(String filePath) async {
if (Platform.isWindows) {
// Launch installer
await Process.start(filePath, []);
// Show message to user
if (await _showRestartDialog()) {
// Close current app
exit(0);
}
} else if (Platform.isAndroid) {
// Open APK for installation
final result = await OpenFile.open(filePath);
if (result.type != ResultType.done) {
throw Exception('Failed to open installer: ${result.message}');
}
} else if (Platform.isLinux) {
if (filePath.endsWith('.AppImage')) {
// Make executable
await Process.run('chmod', ['+x', filePath]);
// Launch new version
await Process.start(filePath, []);
exit(0);
} else if (filePath.endsWith('.deb')) {
// Install via dpkg
await Process.run('pkexec', ['dpkg', '-i', filePath]);
}
}
}
} Update Settings
Allow users to configure update behavior:
// lib/features/settings/presentation/widgets/update_settings.dart
class UpdateSettings extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<SettingsBloc, SettingsState>(
builder: (context, state) {
return Column(
children: [
SwitchListTile(
title: Text('Automatic Update Check'),
subtitle: Text('Check for updates on app start'),
value: state.autoCheckUpdates,
onChanged: (value) {
context.read<SettingsBloc>().add(
UpdateSettingEvent(autoCheckUpdates: value),
);
},
),
SwitchListTile(
title: Text('Update Notifications'),
subtitle: Text('Show notification when update is available'),
value: state.updateNotifications,
onChanged: (value) {
context.read<SettingsBloc>().add(
UpdateSettingEvent(updateNotifications: value),
);
},
),
ListTile(
title: Text('Check for Updates'),
subtitle: Text('Manually check for new version'),
trailing: Icon(Icons.refresh),
onTap: () async {
final updateService = context.read<UpdateService>();
final release = await updateService.checkForUpdates();
if (release != null) {
showDialog(
context: context,
builder: (context) => UpdateDialog(release: release),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('You\'re up to date!')),
);
}
},
),
],
);
},
);
}
} Background Update Check
Implement periodic background checks using WorkManager (Android) or similar:
// lib/core/services/background_update_service.dart
class BackgroundUpdateService {
Future<void> scheduleUpdateCheck() async {
if (Platform.isAndroid) {
await Workmanager().registerPeriodicTask(
'update-check',
'checkForUpdates',
frequency: Duration(hours: 6),
constraints: Constraints(
networkType: NetworkType.connected,
),
);
} else {
// For desktop, check on app start
WidgetsBinding.instance.addPostFrameCallback((_) {
_checkForUpdates();
});
}
}
Future<void> _checkForUpdates() async {
final updateService = GetIt.I<UpdateService>();
final release = await updateService.checkForUpdates();
if (release != null) {
// Show notification
await _showUpdateNotification(release);
}
}
Future<void> _showUpdateNotification(Release release) async {
final flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
await flutterLocalNotificationsPlugin.show(
0,
'Update Available',
'MiyoList ${release.tagName} is now available',
NotificationDetails(
android: AndroidNotificationDetails(
'updates',
'Updates',
channelDescription: 'App update notifications',
importance: Importance.high,
priority: Priority.high,
),
),
);
}
} Testing
Manual Testing
- Lower app version: Temporarily change version in pubspec.yaml to test update detection
- Create test release: Create a pre-release on GitHub with higher version
- Test notification: Verify update dialog appears with correct changelog
- Test download: Download installer and verify file integrity
- Test installation: Install update and verify it works correctly
Automated Testing
// test/core/services/update_service_test.dart
void main() {
group('UpdateService', () {
late UpdateService updateService;
late MockDio mockDio;
late MockPackageInfo mockPackageInfo;
setUp(() {
mockDio = MockDio();
mockPackageInfo = MockPackageInfo();
updateService = UpdateService(
dio: mockDio,
packageInfo: mockPackageInfo,
);
});
test('should detect new version available', () async {
// Arrange
when(() => mockPackageInfo.version).thenReturn('1.0.0');
when(() => mockDio.get(any())).thenAnswer(
(_) async => Response(
data: {
'tag_name': 'v1.1.0',
'name': 'Release 1.1.0',
'body': 'New features',
'assets': [],
'published_at': DateTime.now().toIso8601String(),
},
statusCode: 200,
),
);
// Act
final release = await updateService.checkForUpdates();
// Assert
expect(release, isNotNull);
expect(release?.tagName, 'v1.1.0');
});
test('should return null when up to date', () async {
// Arrange
when(() => mockPackageInfo.version).thenReturn('1.1.0');
when(() => mockDio.get(any())).thenAnswer(
(_) async => Response(
data: {
'tag_name': 'v1.0.0',
// ... other fields
},
statusCode: 200,
),
);
// Act
final release = await updateService.checkForUpdates();
// Assert
expect(release, isNull);
});
});
} Best Practices
- ✓ Use semantic versioning: Follow SemVer (MAJOR.MINOR.PATCH) for releases
- ✓ Include changelogs: Always provide detailed release notes in GitHub releases
- ✓ Test installers: Verify installers work on fresh systems before releasing
- ✓ Handle errors gracefully: Don't crash if update check fails
- ✓ Respect user choice: Allow users to skip updates or disable auto-check
- ✓ Sign installers: Code sign Windows/Mac installers for security
- ✓ Backup data: Remind users to backup before major updates