← Back to Documentation

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

1️⃣

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']),
    );
  }
}
2️⃣

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'),
        ),
      ],
    );
  }
}
3️⃣

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;
  }
}
4️⃣

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

  1. Lower app version: Temporarily change version in pubspec.yaml to test update detection
  2. Create test release: Create a pre-release on GitHub with higher version
  3. Test notification: Verify update dialog appears with correct changelog
  4. Test download: Download installer and verify file integrity
  5. 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

Related Resources