mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:27:09 -05:00 
			
		
		
		
	feat(mobile): Cache assets and albums for faster loading speed
feat(mobile): Cache assets and albums for faster loading speed
This commit is contained in:
		
						commit
						061b229e12
					
				@ -1,22 +1,35 @@
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/album/services/album_cache.service.dart';
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
 | 
			
		||||
class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
 | 
			
		||||
  AlbumNotifier(this._albumService) : super([]);
 | 
			
		||||
  AlbumNotifier(this._albumService, this._albumCacheService) : super([]);
 | 
			
		||||
  final AlbumService _albumService;
 | 
			
		||||
  final AlbumCacheService _albumCacheService;
 | 
			
		||||
 | 
			
		||||
  _cacheState() {
 | 
			
		||||
    _albumCacheService.put(state);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getAllAlbums() async {
 | 
			
		||||
 | 
			
		||||
    if (await _albumCacheService.isValid() && state.isEmpty) {
 | 
			
		||||
      state = await _albumCacheService.get();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    List<AlbumResponseDto>? albums =
 | 
			
		||||
        await _albumService.getAlbums(isShared: false);
 | 
			
		||||
 | 
			
		||||
    if (albums != null) {
 | 
			
		||||
      state = albums;
 | 
			
		||||
      _cacheState();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  deleteAlbum(String albumId) {
 | 
			
		||||
    state = state.where((album) => album.id != albumId).toList();
 | 
			
		||||
    _cacheState();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<AlbumResponseDto?> createAlbum(
 | 
			
		||||
@ -28,6 +41,8 @@ class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
 | 
			
		||||
 | 
			
		||||
    if (album != null) {
 | 
			
		||||
      state = [...state, album];
 | 
			
		||||
      _cacheState();
 | 
			
		||||
 | 
			
		||||
      return album;
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
@ -36,5 +51,8 @@ class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
 | 
			
		||||
 | 
			
		||||
final albumProvider =
 | 
			
		||||
    StateNotifierProvider<AlbumNotifier, List<AlbumResponseDto>>((ref) {
 | 
			
		||||
  return AlbumNotifier(ref.watch(albumServiceProvider));
 | 
			
		||||
  return AlbumNotifier(
 | 
			
		||||
    ref.watch(albumServiceProvider),
 | 
			
		||||
    ref.watch(albumCacheServiceProvider),
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -1,12 +1,18 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/album/services/album.service.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/album/services/album_cache.service.dart';
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
 | 
			
		||||
class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
 | 
			
		||||
  SharedAlbumNotifier(this._sharedAlbumService) : super([]);
 | 
			
		||||
  SharedAlbumNotifier(this._sharedAlbumService, this._sharedAlbumCacheService) : super([]);
 | 
			
		||||
 | 
			
		||||
  final AlbumService _sharedAlbumService;
 | 
			
		||||
  final SharedAlbumCacheService _sharedAlbumCacheService;
 | 
			
		||||
 | 
			
		||||
  _cacheState() {
 | 
			
		||||
    _sharedAlbumCacheService.put(state);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<AlbumResponseDto?> createSharedAlbum(
 | 
			
		||||
    String albumName,
 | 
			
		||||
@ -22,6 +28,7 @@ class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
 | 
			
		||||
 | 
			
		||||
      if (newAlbum != null) {
 | 
			
		||||
        state = [...state, newAlbum];
 | 
			
		||||
        _cacheState();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return newAlbum;
 | 
			
		||||
@ -33,16 +40,22 @@ class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getAllSharedAlbums() async {
 | 
			
		||||
    if (await _sharedAlbumCacheService.isValid() && state.isEmpty) {
 | 
			
		||||
      state = await _sharedAlbumCacheService.get();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    List<AlbumResponseDto>? sharedAlbums =
 | 
			
		||||
        await _sharedAlbumService.getAlbums(isShared: true);
 | 
			
		||||
 | 
			
		||||
    if (sharedAlbums != null) {
 | 
			
		||||
      state = sharedAlbums;
 | 
			
		||||
      _cacheState();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  deleteAlbum(String albumId) async {
 | 
			
		||||
    state = state.where((album) => album.id != albumId).toList();
 | 
			
		||||
    _cacheState();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<bool> leaveAlbum(String albumId) async {
 | 
			
		||||
@ -50,6 +63,7 @@ class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
 | 
			
		||||
 | 
			
		||||
    if (res) {
 | 
			
		||||
      state = state.where((album) => album.id != albumId).toList();
 | 
			
		||||
      _cacheState();
 | 
			
		||||
      return true;
 | 
			
		||||
    } else {
 | 
			
		||||
      return false;
 | 
			
		||||
@ -72,7 +86,10 @@ class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
 | 
			
		||||
 | 
			
		||||
final sharedAlbumProvider =
 | 
			
		||||
    StateNotifierProvider<SharedAlbumNotifier, List<AlbumResponseDto>>((ref) {
 | 
			
		||||
  return SharedAlbumNotifier(ref.watch(albumServiceProvider));
 | 
			
		||||
  return SharedAlbumNotifier(
 | 
			
		||||
    ref.watch(albumServiceProvider),
 | 
			
		||||
    ref.watch(sharedAlbumCacheServiceProvider),
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
final sharedAlbumDetailProvider = FutureProvider.autoDispose
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										49
									
								
								mobile/lib/modules/album/services/album_cache.service.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								mobile/lib/modules/album/services/album_cache.service.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,49 @@
 | 
			
		||||
 | 
			
		||||
import 'package:collection/collection.dart';
 | 
			
		||||
import 'package:flutter/foundation.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/services/json_cache.dart';
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
 | 
			
		||||
class BaseAlbumCacheService extends JsonCache<List<AlbumResponseDto>> {
 | 
			
		||||
  BaseAlbumCacheService(super.cacheFileName);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void put(List<AlbumResponseDto> data) {
 | 
			
		||||
    putRawData(data.map((e) => e.toJson()).toList());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<List<AlbumResponseDto>> get() async {
 | 
			
		||||
    try {
 | 
			
		||||
      final mapList = await readRawData() as List<dynamic>;
 | 
			
		||||
 | 
			
		||||
      final responseData = mapList
 | 
			
		||||
          .map((e) => AlbumResponseDto.fromJson(e))
 | 
			
		||||
          .whereNotNull()
 | 
			
		||||
          .toList();
 | 
			
		||||
 | 
			
		||||
      return responseData;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      debugPrint(e.toString());
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class AlbumCacheService extends BaseAlbumCacheService {
 | 
			
		||||
  AlbumCacheService() : super("album_cache");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class SharedAlbumCacheService extends BaseAlbumCacheService {
 | 
			
		||||
  SharedAlbumCacheService() : super("shared_album_cache");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
final albumCacheServiceProvider = Provider(
 | 
			
		||||
      (ref) => AlbumCacheService(),
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
final sharedAlbumCacheServiceProvider = Provider(
 | 
			
		||||
      (ref) => SharedAlbumCacheService(),
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										37
									
								
								mobile/lib/modules/home/services/asset_cache.service.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								mobile/lib/modules/home/services/asset_cache.service.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,37 @@
 | 
			
		||||
import 'package:collection/collection.dart';
 | 
			
		||||
import 'package:flutter/foundation.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:immich_mobile/shared/services/json_cache.dart';
 | 
			
		||||
import 'package:openapi/api.dart';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AssetCacheService extends JsonCache<List<AssetResponseDto>> {
 | 
			
		||||
  AssetCacheService() : super("asset_cache");
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void put(List<AssetResponseDto> data) {
 | 
			
		||||
    putRawData(data.map((e) => e.toJson()).toList());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<List<AssetResponseDto>> get() async {
 | 
			
		||||
    try {
 | 
			
		||||
      final mapList = await readRawData() as List<dynamic>;
 | 
			
		||||
 | 
			
		||||
      final responseData = mapList
 | 
			
		||||
          .map((e) => AssetResponseDto.fromJson(e))
 | 
			
		||||
          .whereNotNull()
 | 
			
		||||
          .toList();
 | 
			
		||||
 | 
			
		||||
      return responseData;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      debugPrint(e.toString());
 | 
			
		||||
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
final assetCacheServiceProvider = Provider(
 | 
			
		||||
      (ref) => AssetCacheService(),
 | 
			
		||||
);
 | 
			
		||||
@ -3,6 +3,8 @@ import 'package:flutter/services.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/album/services/album_cache.service.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/home/services/asset_cache.service.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
 | 
			
		||||
import 'package:immich_mobile/modules/backup/services/backup.service.dart';
 | 
			
		||||
@ -16,6 +18,9 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
 | 
			
		||||
    this._deviceInfoService,
 | 
			
		||||
    this._backupService,
 | 
			
		||||
    this._apiService,
 | 
			
		||||
    this._assetCacheService,
 | 
			
		||||
    this._albumCacheService,
 | 
			
		||||
    this._sharedAlbumCacheService,
 | 
			
		||||
  ) : super(
 | 
			
		||||
          AuthenticationState(
 | 
			
		||||
            deviceId: "",
 | 
			
		||||
@ -42,6 +47,9 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
 | 
			
		||||
  final DeviceInfoService _deviceInfoService;
 | 
			
		||||
  final BackupService _backupService;
 | 
			
		||||
  final ApiService _apiService;
 | 
			
		||||
  final AssetCacheService _assetCacheService;
 | 
			
		||||
  final AlbumCacheService _albumCacheService;
 | 
			
		||||
  final SharedAlbumCacheService _sharedAlbumCacheService;
 | 
			
		||||
 | 
			
		||||
  Future<bool> login(
 | 
			
		||||
    String email,
 | 
			
		||||
@ -153,7 +161,9 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
 | 
			
		||||
  Future<bool> logout() async {
 | 
			
		||||
    Hive.box(userInfoBox).delete(accessTokenKey);
 | 
			
		||||
    state = state.copyWith(isAuthenticated: false);
 | 
			
		||||
 | 
			
		||||
    _assetCacheService.invalidate();
 | 
			
		||||
    _albumCacheService.invalidate();
 | 
			
		||||
    _sharedAlbumCacheService.invalidate();
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -199,5 +209,8 @@ final authenticationProvider =
 | 
			
		||||
    ref.watch(deviceInfoServiceProvider),
 | 
			
		||||
    ref.watch(backupServiceProvider),
 | 
			
		||||
    ref.watch(apiServiceProvider),
 | 
			
		||||
    ref.watch(assetCacheServiceProvider),
 | 
			
		||||
    ref.watch(albumCacheServiceProvider),
 | 
			
		||||
    ref.watch(sharedAlbumCacheServiceProvider),
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,7 @@
 | 
			
		||||
import 'package:flutter/foundation.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.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/shared/services/device_info.service.dart';
 | 
			
		||||
import 'package:collection/collection.dart';
 | 
			
		||||
import 'package:intl/intl.dart';
 | 
			
		||||
@ -9,24 +10,50 @@ import 'package:photo_manager/photo_manager.dart';
 | 
			
		||||
 | 
			
		||||
class AssetNotifier extends StateNotifier<List<AssetResponseDto>> {
 | 
			
		||||
  final AssetService _assetService;
 | 
			
		||||
  final AssetCacheService _assetCacheService;
 | 
			
		||||
 | 
			
		||||
  final DeviceInfoService _deviceInfoService = DeviceInfoService();
 | 
			
		||||
 | 
			
		||||
  AssetNotifier(this._assetService) : super([]);
 | 
			
		||||
  AssetNotifier(this._assetService, this._assetCacheService) : super([]);
 | 
			
		||||
 | 
			
		||||
  _cacheState() {
 | 
			
		||||
    _assetCacheService.put(state);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getAllAsset() async {
 | 
			
		||||
    final stopwatch = Stopwatch();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    if (await _assetCacheService.isValid() && state.isEmpty) {
 | 
			
		||||
      stopwatch.start();
 | 
			
		||||
      state = await _assetCacheService.get();
 | 
			
		||||
      debugPrint("Reading assets from cache: ${stopwatch.elapsedMilliseconds}ms");
 | 
			
		||||
      stopwatch.reset();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    stopwatch.start();
 | 
			
		||||
    var allAssets = await _assetService.getAllAsset();
 | 
			
		||||
    debugPrint("Query assets from API: ${stopwatch.elapsedMilliseconds}ms");
 | 
			
		||||
    stopwatch.reset();
 | 
			
		||||
 | 
			
		||||
    if (allAssets != null) {
 | 
			
		||||
      state = allAssets;
 | 
			
		||||
 | 
			
		||||
      stopwatch.start();
 | 
			
		||||
      _cacheState();
 | 
			
		||||
      debugPrint("Store assets in cache: ${stopwatch.elapsedMilliseconds}ms");
 | 
			
		||||
      stopwatch.reset();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  clearAllAsset() {
 | 
			
		||||
    state = [];
 | 
			
		||||
    _cacheState();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onNewAssetUploaded(AssetResponseDto newAsset) {
 | 
			
		||||
    state = [...state, newAsset];
 | 
			
		||||
    _cacheState();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  deleteAssets(Set<AssetResponseDto> deleteAssets) async {
 | 
			
		||||
@ -65,12 +92,15 @@ class AssetNotifier extends StateNotifier<List<AssetResponseDto>> {
 | 
			
		||||
            state.where((immichAsset) => immichAsset.id != asset.id).toList();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    _cacheState();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
final assetProvider =
 | 
			
		||||
    StateNotifierProvider<AssetNotifier, List<AssetResponseDto>>((ref) {
 | 
			
		||||
  return AssetNotifier(ref.watch(assetServiceProvider));
 | 
			
		||||
  return AssetNotifier(
 | 
			
		||||
      ref.watch(assetServiceProvider), ref.watch(assetCacheServiceProvider));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
final assetGroupByDateTimeProvider = StateProvider((ref) {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										49
									
								
								mobile/lib/shared/services/json_cache.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								mobile/lib/shared/services/json_cache.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,49 @@
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
import 'dart:io';
 | 
			
		||||
 | 
			
		||||
import 'package:path_provider/path_provider.dart';
 | 
			
		||||
 | 
			
		||||
abstract class JsonCache<T> {
 | 
			
		||||
  final String cacheFileName;
 | 
			
		||||
 | 
			
		||||
  JsonCache(this.cacheFileName);
 | 
			
		||||
 | 
			
		||||
  Future<File> _getCacheFile() async {
 | 
			
		||||
    final basePath = await getTemporaryDirectory();
 | 
			
		||||
    final basePathName = basePath.path;
 | 
			
		||||
 | 
			
		||||
    final file = File("$basePathName/$cacheFileName.bin");
 | 
			
		||||
 | 
			
		||||
    return file;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<bool> isValid() async {
 | 
			
		||||
    final file = await _getCacheFile();
 | 
			
		||||
    return await file.exists();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> invalidate() async {
 | 
			
		||||
    final file = await _getCacheFile();
 | 
			
		||||
    await file.delete();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> putRawData(dynamic data) async {
 | 
			
		||||
    final jsonString = json.encode(data);
 | 
			
		||||
    final file = await _getCacheFile();
 | 
			
		||||
 | 
			
		||||
    if (!await file.exists()) {
 | 
			
		||||
      await file.create();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    await file.writeAsString(jsonString);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  dynamic readRawData() async {
 | 
			
		||||
    final file = await _getCacheFile();
 | 
			
		||||
    final data = await file.readAsString();
 | 
			
		||||
    return json.decode(data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void put(T data);
 | 
			
		||||
  Future<T> get();
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user