mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
feat(mobile): Various minor performance improvements (#1176)
* Improve scroll performance by introducing repaint boundaries and moving more calculations to providers. * Add error handing for malformed dates. * Remove unused method * Use compute in different places to improve app performance during heavy tasks * Fix test * Refactor `List<RenderAssetGridElement>` to separate `RenderList` class and make `fromAssetGroups` a static method of this class. * Fix loading indicator bug * Use provider directly * `RenderList` refactoring * `AssetNotifier` refactoring * Move `combine` to static private method * Extract compute methods in cache services to static private methods. * Use `tryParse` instead of `parse` with try/catch for dates. * Fix bug in caching mechanism. * Fixed state not being used to trigger conditional rendering * styling * Corrected state Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
@@ -1,10 +1,14 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/hive_box.dart';
|
||||
import 'package:immich_mobile/modules/home/services/asset.service.dart';
|
||||
import 'package:immich_mobile/modules/home/services/asset_cache.service.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/services/device_info.service.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
@@ -14,18 +18,79 @@ import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
class AssetNotifier extends StateNotifier<List<Asset>> {
|
||||
class AssetsState {
|
||||
final List<Asset> allAssets;
|
||||
final RenderList? renderList;
|
||||
|
||||
AssetsState(this.allAssets, {this.renderList});
|
||||
|
||||
Future<AssetsState> withRenderDataStructure(int groupSize) async {
|
||||
return AssetsState(
|
||||
allAssets,
|
||||
renderList:
|
||||
await RenderList.fromAssetGroups(await _groupByDate(), groupSize),
|
||||
);
|
||||
}
|
||||
|
||||
AssetsState withAdditionalAssets(List<Asset> toAdd) {
|
||||
return AssetsState([...allAssets, ...toAdd]);
|
||||
}
|
||||
|
||||
_groupByDate() async {
|
||||
sortCompare(List<Asset> assets) {
|
||||
assets.sortByCompare<DateTime>(
|
||||
(e) => e.createdAt,
|
||||
(a, b) => b.compareTo(a),
|
||||
);
|
||||
return assets.groupListsBy(
|
||||
(element) => DateFormat('y-MM-dd').format(element.createdAt.toLocal()),
|
||||
);
|
||||
}
|
||||
|
||||
return await compute(sortCompare, allAssets.toList());
|
||||
}
|
||||
|
||||
static fromAssetList(List<Asset> assets) {
|
||||
return AssetsState(assets);
|
||||
}
|
||||
|
||||
static empty() {
|
||||
return AssetsState([]);
|
||||
}
|
||||
}
|
||||
|
||||
class _CombineAssetsComputeParameters {
|
||||
final Iterable<Asset> local;
|
||||
final Iterable<Asset> remote;
|
||||
final String deviceId;
|
||||
|
||||
_CombineAssetsComputeParameters(this.local, this.remote, this.deviceId);
|
||||
}
|
||||
|
||||
class AssetNotifier extends StateNotifier<AssetsState> {
|
||||
final AssetService _assetService;
|
||||
final AssetCacheService _assetCacheService;
|
||||
final AppSettingsService _settingsService;
|
||||
final log = Logger('AssetNotifier');
|
||||
final DeviceInfoService _deviceInfoService = DeviceInfoService();
|
||||
bool _getAllAssetInProgress = false;
|
||||
bool _deleteInProgress = false;
|
||||
|
||||
AssetNotifier(this._assetService, this._assetCacheService) : super([]);
|
||||
AssetNotifier(
|
||||
this._assetService,
|
||||
this._assetCacheService,
|
||||
this._settingsService,
|
||||
) : super(AssetsState.fromAssetList([]));
|
||||
|
||||
_cacheState() {
|
||||
_assetCacheService.put(state);
|
||||
_updateAssetsState(List<Asset> newAssetList, {bool cache = true}) async {
|
||||
if (cache) {
|
||||
_assetCacheService.put(newAssetList);
|
||||
}
|
||||
|
||||
state =
|
||||
await AssetsState.fromAssetList(newAssetList).withRenderDataStructure(
|
||||
_settingsService.getSetting(AppSettingsEnum.tilesPerRow),
|
||||
);
|
||||
}
|
||||
|
||||
getAllAsset() async {
|
||||
@@ -43,17 +108,19 @@ class AssetNotifier extends StateNotifier<List<Asset>> {
|
||||
final remoteTask = _assetService.getRemoteAssets(
|
||||
etag: isCacheValid ? box.get(assetEtagKey) : null,
|
||||
);
|
||||
if (isCacheValid && state.isEmpty) {
|
||||
state = await _assetCacheService.get();
|
||||
if (isCacheValid && state.allAssets.isEmpty) {
|
||||
await _updateAssetsState(await _assetCacheService.get(), cache: false);
|
||||
log.info(
|
||||
"Reading assets from cache: ${stopwatch.elapsedMilliseconds}ms",
|
||||
"Reading assets ${state.allAssets.length} from cache: ${stopwatch.elapsedMilliseconds}ms",
|
||||
);
|
||||
stopwatch.reset();
|
||||
}
|
||||
|
||||
int remoteBegin = state.indexWhere((a) => a.isRemote);
|
||||
remoteBegin = remoteBegin == -1 ? state.length : remoteBegin;
|
||||
final List<Asset> currentLocal = state.slice(0, remoteBegin);
|
||||
int remoteBegin = state.allAssets.indexWhere((a) => a.isRemote);
|
||||
remoteBegin = remoteBegin == -1 ? state.allAssets.length : remoteBegin;
|
||||
|
||||
final List<Asset> currentLocal = state.allAssets.slice(0, remoteBegin);
|
||||
|
||||
final Pair<List<Asset>?, String?> remoteResult = await remoteTask;
|
||||
List<Asset>? newRemote = remoteResult.first;
|
||||
List<Asset>? newLocal = await localTask;
|
||||
@@ -64,27 +131,32 @@ class AssetNotifier extends StateNotifier<List<Asset>> {
|
||||
log.info("state is already up-to-date");
|
||||
return;
|
||||
}
|
||||
newRemote ??= state.slice(remoteBegin);
|
||||
newRemote ??= state.allAssets.slice(remoteBegin);
|
||||
newLocal ??= [];
|
||||
state = _combineLocalAndRemoteAssets(local: newLocal, remote: newRemote);
|
||||
|
||||
final combinedAssets = await _combineLocalAndRemoteAssets(
|
||||
local: newLocal,
|
||||
remote: newRemote,
|
||||
);
|
||||
await _updateAssetsState(combinedAssets);
|
||||
|
||||
log.info("Combining assets: ${stopwatch.elapsedMilliseconds}ms");
|
||||
|
||||
stopwatch.reset();
|
||||
_cacheState();
|
||||
box.put(assetEtagKey, remoteResult.second);
|
||||
log.info("Store assets in cache: ${stopwatch.elapsedMilliseconds}ms");
|
||||
} finally {
|
||||
_getAllAssetInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
List<Asset> _combineLocalAndRemoteAssets({
|
||||
required Iterable<Asset> local,
|
||||
required List<Asset> remote,
|
||||
}) {
|
||||
static Future<List<Asset>> _computeCombine(
|
||||
_CombineAssetsComputeParameters data,
|
||||
) async {
|
||||
var local = data.local;
|
||||
var remote = data.remote;
|
||||
final deviceId = data.deviceId;
|
||||
|
||||
final List<Asset> assets = [];
|
||||
if (remote.isNotEmpty && local.isNotEmpty) {
|
||||
final String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
||||
final Set<String> existingIds = remote
|
||||
.where((e) => e.deviceId == deviceId)
|
||||
.map((e) => e.deviceAssetId)
|
||||
@@ -97,31 +169,40 @@ class AssetNotifier extends StateNotifier<List<Asset>> {
|
||||
return assets;
|
||||
}
|
||||
|
||||
Future<List<Asset>> _combineLocalAndRemoteAssets({
|
||||
required Iterable<Asset> local,
|
||||
required List<Asset> remote,
|
||||
}) async {
|
||||
final String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
|
||||
return await compute(
|
||||
_computeCombine,
|
||||
_CombineAssetsComputeParameters(local, remote, deviceId),
|
||||
);
|
||||
}
|
||||
|
||||
clearAllAsset() {
|
||||
state = [];
|
||||
_cacheState();
|
||||
_updateAssetsState([]);
|
||||
}
|
||||
|
||||
onNewAssetUploaded(AssetResponseDto newAsset) {
|
||||
final int i = state.indexWhere(
|
||||
final int i = state.allAssets.indexWhere(
|
||||
(a) =>
|
||||
a.isRemote ||
|
||||
(a.id == newAsset.deviceAssetId && a.deviceId == newAsset.deviceId),
|
||||
);
|
||||
|
||||
if (i == -1 || state[i].deviceAssetId != newAsset.deviceAssetId) {
|
||||
state = [...state, Asset.remote(newAsset)];
|
||||
if (i == -1 || state.allAssets[i].deviceAssetId != newAsset.deviceAssetId) {
|
||||
_updateAssetsState([...state.allAssets, Asset.remote(newAsset)]);
|
||||
} else {
|
||||
// order is important to keep all local-only assets at the beginning!
|
||||
state = [
|
||||
...state.slice(0, i),
|
||||
...state.slice(i + 1),
|
||||
_updateAssetsState([
|
||||
...state.allAssets.slice(0, i),
|
||||
...state.allAssets.slice(i + 1),
|
||||
Asset.remote(newAsset),
|
||||
];
|
||||
]);
|
||||
// TODO here is a place to unify local/remote assets by replacing the
|
||||
// local-only asset in the state with a local&remote asset
|
||||
}
|
||||
_cacheState();
|
||||
}
|
||||
|
||||
deleteAssets(Set<Asset> deleteAssets) async {
|
||||
@@ -133,8 +214,9 @@ class AssetNotifier extends StateNotifier<List<Asset>> {
|
||||
deleted.addAll(localDeleted);
|
||||
deleted.addAll(remoteDeleted);
|
||||
if (deleted.isNotEmpty) {
|
||||
state = state.where((a) => !deleted.contains(a.id)).toList();
|
||||
_cacheState();
|
||||
_updateAssetsState(
|
||||
state.allAssets.where((a) => !deleted.contains(a.id)).toList(),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
_deleteInProgress = false;
|
||||
@@ -180,23 +262,11 @@ class AssetNotifier extends StateNotifier<List<Asset>> {
|
||||
}
|
||||
}
|
||||
|
||||
final assetProvider = StateNotifierProvider<AssetNotifier, List<Asset>>((ref) {
|
||||
final assetProvider = StateNotifierProvider<AssetNotifier, AssetsState>((ref) {
|
||||
return AssetNotifier(
|
||||
ref.watch(assetServiceProvider),
|
||||
ref.watch(assetCacheServiceProvider),
|
||||
);
|
||||
});
|
||||
|
||||
final assetGroupByDateTimeProvider = StateProvider((ref) {
|
||||
final assets = ref.watch(assetProvider).toList();
|
||||
// `toList()` ist needed to make a copy as to NOT sort the original list/state
|
||||
|
||||
assets.sortByCompare<DateTime>(
|
||||
(e) => e.createdAt,
|
||||
(a, b) => b.compareTo(a),
|
||||
);
|
||||
return assets.groupListsBy(
|
||||
(element) => DateFormat('y-MM-dd').format(element.createdAt.toLocal()),
|
||||
ref.watch(appSettingsServiceProvider),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -204,7 +274,8 @@ final assetGroupByMonthYearProvider = StateProvider((ref) {
|
||||
// TODO: remove `where` once temporary workaround is no longer needed (to only
|
||||
// allow remote assets to be added to album). Keep `toList()` as to NOT sort
|
||||
// the original list/state
|
||||
final assets = ref.watch(assetProvider).where((e) => e.isRemote).toList();
|
||||
final assets =
|
||||
ref.watch(assetProvider).allAssets.where((e) => e.isRemote).toList();
|
||||
|
||||
assets.sortByCompare<DateTime>(
|
||||
(e) => e.createdAt,
|
||||
|
||||
Reference in New Issue
Block a user