From a57c4d9a9e2739c00e37c2a5c1277a72fca87049 Mon Sep 17 00:00:00 2001
From: Peter Ombodi
Date: Fri, 26 Dec 2025 20:44:07 +0200
Subject: [PATCH] fix(drift backup notifier): add lifecycle guards and dispose
logging (#24806)
* fix(drift backup notifier): add lifecycle guards and dispose logging
* fix(drift backup notifier): re-read notifiers in callbacks to avoid disposed backup notifier
* fix(drift backup notifier): increase the log level to warning.
---------
Co-authored-by: Peter Ombodi
---
.../providers/background_sync.provider.dart | 11 +++--
.../backup/drift_backup.provider.dart | 44 ++++++++++++++++++-
2 files changed, 50 insertions(+), 5 deletions(-)
diff --git a/mobile/lib/providers/background_sync.provider.dart b/mobile/lib/providers/background_sync.provider.dart
index a61cd93022..5d6a2f0f4d 100644
--- a/mobile/lib/providers/background_sync.provider.dart
+++ b/mobile/lib/providers/background_sync.provider.dart
@@ -5,16 +5,21 @@ import 'package:immich_mobile/providers/sync_status.provider.dart';
final backgroundSyncProvider = Provider((ref) {
final syncStatusNotifier = ref.read(syncStatusProvider.notifier);
- final backupProvider = ref.read(driftBackupProvider.notifier);
final manager = BackgroundSyncManager(
onRemoteSyncStart: () {
syncStatusNotifier.startRemoteSync();
- backupProvider.updateError(BackupError.none);
+ final backupProvider = ref.read(driftBackupProvider.notifier);
+ if (backupProvider.mounted) {
+ backupProvider.updateError(BackupError.none);
+ }
},
onRemoteSyncComplete: (isSuccess) {
syncStatusNotifier.completeRemoteSync();
- backupProvider.updateError(isSuccess == true ? BackupError.none : BackupError.syncFailed);
+ final backupProvider = ref.read(driftBackupProvider.notifier);
+ if (backupProvider.mounted) {
+ backupProvider.updateError(isSuccess == true ? BackupError.none : BackupError.syncFailed);
+ }
},
onRemoteSyncError: syncStatusNotifier.errorRemoteSync,
onLocalSyncStart: syncStatusNotifier.startLocalSync,
diff --git a/mobile/lib/providers/backup/drift_backup.provider.dart b/mobile/lib/providers/backup/drift_backup.provider.dart
index f52fc654f2..ec427613f1 100644
--- a/mobile/lib/providers/backup/drift_backup.provider.dart
+++ b/mobile/lib/providers/backup/drift_backup.provider.dart
@@ -212,8 +212,8 @@ class DriftBackupNotifier extends StateNotifier {
),
) {
{
- _uploadService.taskStatusStream.listen(_handleTaskStatusUpdate);
- _uploadService.taskProgressStream.listen(_handleTaskProgressUpdate);
+ _statusSubscription = _uploadService.taskStatusStream.listen(_handleTaskStatusUpdate);
+ _progressSubscription = _uploadService.taskProgressStream.listen(_handleTaskProgressUpdate);
}
}
@@ -224,6 +224,10 @@ class DriftBackupNotifier extends StateNotifier {
/// Remove upload item from state
void _removeUploadItem(String taskId) {
+ if (!mounted) {
+ _logger.warning("Skip _removeUploadItem: notifier disposed");
+ return;
+ }
if (state.uploadItems.containsKey(taskId)) {
final updatedItems = Map.from(state.uploadItems);
updatedItems.remove(taskId);
@@ -232,6 +236,10 @@ class DriftBackupNotifier extends StateNotifier {
}
void _handleTaskStatusUpdate(TaskStatusUpdate update) {
+ if (!mounted) {
+ _logger.warning("Skip _handleTaskStatusUpdate: notifier disposed");
+ return;
+ }
final taskId = update.task.taskId;
switch (update.status) {
@@ -291,6 +299,10 @@ class DriftBackupNotifier extends StateNotifier {
}
void _handleTaskProgressUpdate(TaskProgressUpdate update) {
+ if (!mounted) {
+ _logger.warning("Skip _handleTaskProgressUpdate: notifier disposed");
+ return;
+ }
final taskId = update.task.taskId;
final filename = update.task.displayName;
final progress = update.progress;
@@ -332,7 +344,15 @@ class DriftBackupNotifier extends StateNotifier {
}
Future getBackupStatus(String userId) async {
+ if (!mounted) {
+ _logger.warning("Skip getBackupStatus (pre-call): notifier disposed");
+ return;
+ }
final counts = await _uploadService.getBackupCounts(userId);
+ if (!mounted) {
+ _logger.warning("Skip getBackupStatus (post-call): notifier disposed");
+ return;
+ }
state = state.copyWith(
totalCount: counts.total,
@@ -343,6 +363,10 @@ class DriftBackupNotifier extends StateNotifier {
}
void updateError(BackupError error) async {
+ if (!mounted) {
+ _logger.warning("Skip updateError: notifier disposed");
+ return;
+ }
state = state.copyWith(error: error);
}
@@ -360,10 +384,18 @@ class DriftBackupNotifier extends StateNotifier {
}
Future cancel() async {
+ if (!mounted) {
+ _logger.warning("Skip cancel (pre-call): notifier disposed");
+ return;
+ }
dPrint(() => "Canceling backup tasks...");
state = state.copyWith(enqueueCount: 0, enqueueTotalCount: 0, isCanceling: true, error: BackupError.none);
final activeTaskCount = await _uploadService.cancelBackup();
+ if (!mounted) {
+ _logger.warning("Skip cancel (post-call): notifier disposed");
+ return;
+ }
if (activeTaskCount > 0) {
dPrint(() => "$activeTaskCount tasks left, continuing to cancel...");
@@ -376,9 +408,17 @@ class DriftBackupNotifier extends StateNotifier {
}
Future handleBackupResume(String userId) async {
+ if (!mounted) {
+ _logger.warning("Skip handleBackupResume (pre-call): notifier disposed");
+ return;
+ }
_logger.info("Resuming backup tasks...");
state = state.copyWith(error: BackupError.none);
final tasks = await _uploadService.getActiveTasks(kBackupGroup);
+ if (!mounted) {
+ _logger.warning("Skip handleBackupResume (post-call): notifier disposed");
+ return;
+ }
_logger.info("Found ${tasks.length} tasks");
if (tasks.isEmpty) {