Browse Source

feat(mobile): Cache assets and albums for faster loading speed

feat(mobile): Cache assets and albums for faster loading speed
Alex 2 years ago
parent
commit
061b229e12

+ 20 - 2
mobile/lib/modules/album/providers/album.provider.dart

@@ -1,22 +1,35 @@
 import 'package:hooks_riverpod/hooks_riverpod.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.service.dart';
+import 'package:immich_mobile/modules/album/services/album_cache.service.dart';
 import 'package:openapi/api.dart';
 import 'package:openapi/api.dart';
 
 
 class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
 class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
-  AlbumNotifier(this._albumService) : super([]);
+  AlbumNotifier(this._albumService, this._albumCacheService) : super([]);
   final AlbumService _albumService;
   final AlbumService _albumService;
+  final AlbumCacheService _albumCacheService;
+
+  _cacheState() {
+    _albumCacheService.put(state);
+  }
 
 
   getAllAlbums() async {
   getAllAlbums() async {
+
+    if (await _albumCacheService.isValid() && state.isEmpty) {
+      state = await _albumCacheService.get();
+    }
+
     List<AlbumResponseDto>? albums =
     List<AlbumResponseDto>? albums =
         await _albumService.getAlbums(isShared: false);
         await _albumService.getAlbums(isShared: false);
 
 
     if (albums != null) {
     if (albums != null) {
       state = albums;
       state = albums;
+      _cacheState();
     }
     }
   }
   }
 
 
   deleteAlbum(String albumId) {
   deleteAlbum(String albumId) {
     state = state.where((album) => album.id != albumId).toList();
     state = state.where((album) => album.id != albumId).toList();
+    _cacheState();
   }
   }
 
 
   Future<AlbumResponseDto?> createAlbum(
   Future<AlbumResponseDto?> createAlbum(
@@ -28,6 +41,8 @@ class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
 
 
     if (album != null) {
     if (album != null) {
       state = [...state, album];
       state = [...state, album];
+      _cacheState();
+
       return album;
       return album;
     }
     }
     return null;
     return null;
@@ -36,5 +51,8 @@ class AlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
 
 
 final albumProvider =
 final albumProvider =
     StateNotifierProvider<AlbumNotifier, List<AlbumResponseDto>>((ref) {
     StateNotifierProvider<AlbumNotifier, List<AlbumResponseDto>>((ref) {
-  return AlbumNotifier(ref.watch(albumServiceProvider));
+  return AlbumNotifier(
+    ref.watch(albumServiceProvider),
+    ref.watch(albumCacheServiceProvider),
+  );
 });
 });

+ 19 - 2
mobile/lib/modules/album/providers/shared_album.provider.dart

@@ -1,12 +1,18 @@
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.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.service.dart';
+import 'package:immich_mobile/modules/album/services/album_cache.service.dart';
 import 'package:openapi/api.dart';
 import 'package:openapi/api.dart';
 
 
 class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
 class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
-  SharedAlbumNotifier(this._sharedAlbumService) : super([]);
+  SharedAlbumNotifier(this._sharedAlbumService, this._sharedAlbumCacheService) : super([]);
 
 
   final AlbumService _sharedAlbumService;
   final AlbumService _sharedAlbumService;
+  final SharedAlbumCacheService _sharedAlbumCacheService;
+
+  _cacheState() {
+    _sharedAlbumCacheService.put(state);
+  }
 
 
   Future<AlbumResponseDto?> createSharedAlbum(
   Future<AlbumResponseDto?> createSharedAlbum(
     String albumName,
     String albumName,
@@ -22,6 +28,7 @@ class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
 
 
       if (newAlbum != null) {
       if (newAlbum != null) {
         state = [...state, newAlbum];
         state = [...state, newAlbum];
+        _cacheState();
       }
       }
 
 
       return newAlbum;
       return newAlbum;
@@ -33,16 +40,22 @@ class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
   }
   }
 
 
   getAllSharedAlbums() async {
   getAllSharedAlbums() async {
+    if (await _sharedAlbumCacheService.isValid() && state.isEmpty) {
+      state = await _sharedAlbumCacheService.get();
+    }
+
     List<AlbumResponseDto>? sharedAlbums =
     List<AlbumResponseDto>? sharedAlbums =
         await _sharedAlbumService.getAlbums(isShared: true);
         await _sharedAlbumService.getAlbums(isShared: true);
 
 
     if (sharedAlbums != null) {
     if (sharedAlbums != null) {
       state = sharedAlbums;
       state = sharedAlbums;
+      _cacheState();
     }
     }
   }
   }
 
 
   deleteAlbum(String albumId) async {
   deleteAlbum(String albumId) async {
     state = state.where((album) => album.id != albumId).toList();
     state = state.where((album) => album.id != albumId).toList();
+    _cacheState();
   }
   }
 
 
   Future<bool> leaveAlbum(String albumId) async {
   Future<bool> leaveAlbum(String albumId) async {
@@ -50,6 +63,7 @@ class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
 
 
     if (res) {
     if (res) {
       state = state.where((album) => album.id != albumId).toList();
       state = state.where((album) => album.id != albumId).toList();
+      _cacheState();
       return true;
       return true;
     } else {
     } else {
       return false;
       return false;
@@ -72,7 +86,10 @@ class SharedAlbumNotifier extends StateNotifier<List<AlbumResponseDto>> {
 
 
 final sharedAlbumProvider =
 final sharedAlbumProvider =
     StateNotifierProvider<SharedAlbumNotifier, List<AlbumResponseDto>>((ref) {
     StateNotifierProvider<SharedAlbumNotifier, List<AlbumResponseDto>>((ref) {
-  return SharedAlbumNotifier(ref.watch(albumServiceProvider));
+  return SharedAlbumNotifier(
+    ref.watch(albumServiceProvider),
+    ref.watch(sharedAlbumCacheServiceProvider),
+  );
 });
 });
 
 
 final sharedAlbumDetailProvider = FutureProvider.autoDispose
 final sharedAlbumDetailProvider = FutureProvider.autoDispose

+ 49 - 0
mobile/lib/modules/album/services/album_cache.service.dart

@@ -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 - 0
mobile/lib/modules/home/services/asset_cache.service.dart

@@ -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(),
+);

+ 14 - 1
mobile/lib/modules/login/providers/authentication.provider.dart

@@ -3,6 +3,8 @@ import 'package:flutter/services.dart';
 import 'package:hive/hive.dart';
 import 'package:hive/hive.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/constants/hive_box.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/authentication_state.model.dart';
 import 'package:immich_mobile/modules/login/models/hive_saved_login_info.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';
 import 'package:immich_mobile/modules/backup/services/backup.service.dart';
@@ -16,6 +18,9 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
     this._deviceInfoService,
     this._deviceInfoService,
     this._backupService,
     this._backupService,
     this._apiService,
     this._apiService,
+    this._assetCacheService,
+    this._albumCacheService,
+    this._sharedAlbumCacheService,
   ) : super(
   ) : super(
           AuthenticationState(
           AuthenticationState(
             deviceId: "",
             deviceId: "",
@@ -42,6 +47,9 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
   final DeviceInfoService _deviceInfoService;
   final DeviceInfoService _deviceInfoService;
   final BackupService _backupService;
   final BackupService _backupService;
   final ApiService _apiService;
   final ApiService _apiService;
+  final AssetCacheService _assetCacheService;
+  final AlbumCacheService _albumCacheService;
+  final SharedAlbumCacheService _sharedAlbumCacheService;
 
 
   Future<bool> login(
   Future<bool> login(
     String email,
     String email,
@@ -153,7 +161,9 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
   Future<bool> logout() async {
   Future<bool> logout() async {
     Hive.box(userInfoBox).delete(accessTokenKey);
     Hive.box(userInfoBox).delete(accessTokenKey);
     state = state.copyWith(isAuthenticated: false);
     state = state.copyWith(isAuthenticated: false);
-
+    _assetCacheService.invalidate();
+    _albumCacheService.invalidate();
+    _sharedAlbumCacheService.invalidate();
     return true;
     return true;
   }
   }
 
 
@@ -199,5 +209,8 @@ final authenticationProvider =
     ref.watch(deviceInfoServiceProvider),
     ref.watch(deviceInfoServiceProvider),
     ref.watch(backupServiceProvider),
     ref.watch(backupServiceProvider),
     ref.watch(apiServiceProvider),
     ref.watch(apiServiceProvider),
+    ref.watch(assetCacheServiceProvider),
+    ref.watch(albumCacheServiceProvider),
+    ref.watch(sharedAlbumCacheServiceProvider),
   );
   );
 });
 });

+ 32 - 2
mobile/lib/shared/providers/asset.provider.dart

@@ -1,6 +1,7 @@
 import 'package:flutter/foundation.dart';
 import 'package:flutter/foundation.dart';
 import 'package:hooks_riverpod/hooks_riverpod.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.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:immich_mobile/shared/services/device_info.service.dart';
 import 'package:collection/collection.dart';
 import 'package:collection/collection.dart';
 import 'package:intl/intl.dart';
 import 'package:intl/intl.dart';
@@ -9,24 +10,50 @@ import 'package:photo_manager/photo_manager.dart';
 
 
 class AssetNotifier extends StateNotifier<List<AssetResponseDto>> {
 class AssetNotifier extends StateNotifier<List<AssetResponseDto>> {
   final AssetService _assetService;
   final AssetService _assetService;
+  final AssetCacheService _assetCacheService;
+
   final DeviceInfoService _deviceInfoService = DeviceInfoService();
   final DeviceInfoService _deviceInfoService = DeviceInfoService();
 
 
-  AssetNotifier(this._assetService) : super([]);
+  AssetNotifier(this._assetService, this._assetCacheService) : super([]);
+
+  _cacheState() {
+    _assetCacheService.put(state);
+  }
 
 
   getAllAsset() async {
   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();
     var allAssets = await _assetService.getAllAsset();
+    debugPrint("Query assets from API: ${stopwatch.elapsedMilliseconds}ms");
+    stopwatch.reset();
 
 
     if (allAssets != null) {
     if (allAssets != null) {
       state = allAssets;
       state = allAssets;
+
+      stopwatch.start();
+      _cacheState();
+      debugPrint("Store assets in cache: ${stopwatch.elapsedMilliseconds}ms");
+      stopwatch.reset();
     }
     }
   }
   }
 
 
   clearAllAsset() {
   clearAllAsset() {
     state = [];
     state = [];
+    _cacheState();
   }
   }
 
 
   onNewAssetUploaded(AssetResponseDto newAsset) {
   onNewAssetUploaded(AssetResponseDto newAsset) {
     state = [...state, newAsset];
     state = [...state, newAsset];
+    _cacheState();
   }
   }
 
 
   deleteAssets(Set<AssetResponseDto> deleteAssets) async {
   deleteAssets(Set<AssetResponseDto> deleteAssets) async {
@@ -65,12 +92,15 @@ class AssetNotifier extends StateNotifier<List<AssetResponseDto>> {
             state.where((immichAsset) => immichAsset.id != asset.id).toList();
             state.where((immichAsset) => immichAsset.id != asset.id).toList();
       }
       }
     }
     }
+
+    _cacheState();
   }
   }
 }
 }
 
 
 final assetProvider =
 final assetProvider =
     StateNotifierProvider<AssetNotifier, List<AssetResponseDto>>((ref) {
     StateNotifierProvider<AssetNotifier, List<AssetResponseDto>>((ref) {
-  return AssetNotifier(ref.watch(assetServiceProvider));
+  return AssetNotifier(
+      ref.watch(assetServiceProvider), ref.watch(assetCacheServiceProvider));
 });
 });
 
 
 final assetGroupByDateTimeProvider = StateProvider((ref) {
 final assetGroupByDateTimeProvider = StateProvider((ref) {

+ 49 - 0
mobile/lib/shared/services/json_cache.dart

@@ -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();
+}