diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 10898d51b0..ef9dc8adae 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -115,7 +115,7 @@ jobs:
           flutter-version: "3.10.0"
       - name: Run tests
         working-directory: ./mobile
-        run: flutter test
+        run: flutter test -j 1
 
   generated-api-up-to-date:
     name: Check generated files are up-to-date
diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json
index 03c6aed40b..344b5127d5 100644
--- a/mobile/assets/i18n/en-US.json
+++ b/mobile/assets/i18n/en-US.json
@@ -21,6 +21,7 @@
   "asset_list_layout_settings_group_by": "Group assets by",
   "asset_list_layout_settings_group_by_month": "Month",
   "asset_list_layout_settings_group_by_month_day": "Month + day",
+  "asset_list_layout_settings_group_automatically": "Automatic",
   "asset_list_settings_subtitle": "Photo grid layout settings",
   "asset_list_settings_title": "Photo Grid",
   "backup_album_selection_page_albums_device": "Albums on device ({})",
@@ -276,4 +277,4 @@
   "description_input_hint_text": "Add description...",
   "archive_page_title": "Archive ({})",
   "archive_page_no_archived_assets": "No archived assets found"
-}
+}
\ No newline at end of file
diff --git a/mobile/lib/modules/album/models/asset_selection_page_result.model.dart b/mobile/lib/modules/album/models/asset_selection_page_result.model.dart
index b837efa689..8d8ff5a84b 100644
--- a/mobile/lib/modules/album/models/asset_selection_page_result.model.dart
+++ b/mobile/lib/modules/album/models/asset_selection_page_result.model.dart
@@ -2,47 +2,20 @@ import 'package:collection/collection.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
 
 class AssetSelectionPageResult {
-  final Set<Asset> selectedNewAsset;
-  final Set<Asset> selectedAdditionalAsset;
-  final bool isAlbumExist;
+  final Set<Asset> selectedAssets;
 
   AssetSelectionPageResult({
-    required this.selectedNewAsset,
-    required this.selectedAdditionalAsset,
-    required this.isAlbumExist,
+    required this.selectedAssets,
   });
-
-  AssetSelectionPageResult copyWith({
-    Set<Asset>? selectedNewAsset,
-    Set<Asset>? selectedAdditionalAsset,
-    bool? isAlbumExist,
-  }) {
-    return AssetSelectionPageResult(
-      selectedNewAsset: selectedNewAsset ?? this.selectedNewAsset,
-      selectedAdditionalAsset:
-          selectedAdditionalAsset ?? this.selectedAdditionalAsset,
-      isAlbumExist: isAlbumExist ?? this.isAlbumExist,
-    );
-  }
-
-  @override
-  String toString() =>
-      'AssetSelectionPageResult(selectedNewAsset: $selectedNewAsset, selectedAdditionalAsset: $selectedAdditionalAsset, isAlbumExist: $isAlbumExist)';
-
   @override
   bool operator ==(Object other) {
     if (identical(this, other)) return true;
     final setEquals = const DeepCollectionEquality().equals;
 
     return other is AssetSelectionPageResult &&
-        setEquals(other.selectedNewAsset, selectedNewAsset) &&
-        setEquals(other.selectedAdditionalAsset, selectedAdditionalAsset) &&
-        other.isAlbumExist == isAlbumExist;
+        setEquals(other.selectedAssets, selectedAssets);
   }
 
   @override
-  int get hashCode =>
-      selectedNewAsset.hashCode ^
-      selectedAdditionalAsset.hashCode ^
-      isAlbumExist.hashCode;
+  int get hashCode => selectedAssets.hashCode;
 }
diff --git a/mobile/lib/modules/album/providers/album.provider.dart b/mobile/lib/modules/album/providers/album.provider.dart
index 6011a7a9e9..24679c5178 100644
--- a/mobile/lib/modules/album/providers/album.provider.dart
+++ b/mobile/lib/modules/album/providers/album.provider.dart
@@ -1,4 +1,5 @@
-import 'package:collection/collection.dart';
+import 'dart:async';
+
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/modules/album/services/album.service.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
@@ -9,50 +10,38 @@ import 'package:immich_mobile/shared/providers/db.provider.dart';
 import 'package:isar/isar.dart';
 
 class AlbumNotifier extends StateNotifier<List<Album>> {
-  AlbumNotifier(this._albumService, this._db) : super([]);
+  AlbumNotifier(this._albumService, Isar db) : super([]) {
+    final query = db.albums
+        .filter()
+        .owner((q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId));
+    query.findAll().then((value) => state = value);
+    _streamSub = query.watch().listen((data) => state = data);
+  }
   final AlbumService _albumService;
-  final Isar _db;
+  late final StreamSubscription<List<Album>> _streamSub;
 
-  Future<void> getAllAlbums() async {
-    final User me = Store.get(StoreKey.currentUser);
-    List<Album> albums = await _db.albums
-        .filter()
-        .owner((q) => q.isarIdEqualTo(me.isarId))
-        .findAll();
-    if (!const ListEquality().equals(albums, state)) {
-      state = albums;
-    }
-    await Future.wait([
-      _albumService.refreshDeviceAlbums(),
-      _albumService.refreshRemoteAlbums(isShared: false),
-    ]);
-    albums = await _db.albums
-        .filter()
-        .owner((q) => q.isarIdEqualTo(me.isarId))
-        .findAll();
-    if (!const ListEquality().equals(albums, state)) {
-      state = albums;
-    }
-  }
+  Future<void> getAllAlbums() => Future.wait([
+        _albumService.refreshDeviceAlbums(),
+        _albumService.refreshRemoteAlbums(isShared: false),
+      ]);
 
-  Future<bool> deleteAlbum(Album album) async {
-    state = state.where((a) => a.id != album.id).toList();
-    return _albumService.deleteAlbum(album);
-  }
+  Future<bool> deleteAlbum(Album album) => _albumService.deleteAlbum(album);
 
   Future<Album?> createAlbum(
     String albumTitle,
     Set<Asset> assets,
-  ) async {
-    Album? album = await _albumService.createAlbum(albumTitle, assets, []);
-    if (album != null) {
-      state = [...state, album];
-    }
-    return album;
+  ) =>
+      _albumService.createAlbum(albumTitle, assets, []);
+
+  @override
+  void dispose() {
+    _streamSub.cancel();
+    super.dispose();
   }
 }
 
-final albumProvider = StateNotifierProvider<AlbumNotifier, List<Album>>((ref) {
+final albumProvider =
+    StateNotifierProvider.autoDispose<AlbumNotifier, List<Album>>((ref) {
   return AlbumNotifier(
     ref.watch(albumServiceProvider),
     ref.watch(dbProvider),
diff --git a/mobile/lib/modules/album/providers/asset_selection.provider.dart b/mobile/lib/modules/album/providers/asset_selection.provider.dart
deleted file mode 100644
index 6dfc2a462e..0000000000
--- a/mobile/lib/modules/album/providers/asset_selection.provider.dart
+++ /dev/null
@@ -1,134 +0,0 @@
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/modules/album/models/asset_selection_state.model.dart';
-import 'package:immich_mobile/shared/models/asset.dart';
-
-class AssetSelectionNotifier extends StateNotifier<AssetSelectionState> {
-  AssetSelectionNotifier()
-      : super(
-          AssetSelectionState(
-            selectedNewAssetsForAlbum: {},
-            selectedMonths: {},
-            selectedAdditionalAssetsForAlbum: {},
-            selectedAssetsInAlbumViewer: {},
-            isAlbumExist: false,
-            isMultiselectEnable: false,
-          ),
-        );
-
-  void setIsAlbumExist(bool isAlbumExist) {
-    state = state.copyWith(isAlbumExist: isAlbumExist);
-  }
-
-  void removeAssetsInMonth(
-    String removedMonth,
-    List<Asset> assetsInMonth,
-  ) {
-    Set<Asset> currentAssetList = state.selectedNewAssetsForAlbum;
-    Set<String> currentMonthList = state.selectedMonths;
-
-    currentMonthList
-        .removeWhere((selectedMonth) => selectedMonth == removedMonth);
-
-    for (Asset asset in assetsInMonth) {
-      currentAssetList.removeWhere((e) => e.id == asset.id);
-    }
-
-    state = state.copyWith(
-      selectedNewAssetsForAlbum: currentAssetList,
-      selectedMonths: currentMonthList,
-    );
-  }
-
-  void addAdditionalAssets(List<Asset> assets) {
-    state = state.copyWith(
-      selectedAdditionalAssetsForAlbum: {
-        ...state.selectedAdditionalAssetsForAlbum,
-        ...assets
-      },
-    );
-  }
-
-  void addAllAssetsInMonth(String month, List<Asset> assetsInMonth) {
-    state = state.copyWith(
-      selectedMonths: {...state.selectedMonths, month},
-      selectedNewAssetsForAlbum: {
-        ...state.selectedNewAssetsForAlbum,
-        ...assetsInMonth
-      },
-    );
-  }
-
-  void addNewAssets(Iterable<Asset> assets) {
-    state = state.copyWith(
-      selectedNewAssetsForAlbum: {
-        ...state.selectedNewAssetsForAlbum,
-        ...assets
-      },
-    );
-  }
-
-  void removeSelectedNewAssets(List<Asset> assets) {
-    Set<Asset> currentList = state.selectedNewAssetsForAlbum;
-
-    for (Asset asset in assets) {
-      currentList.removeWhere((e) => e.id == asset.id);
-    }
-
-    state = state.copyWith(selectedNewAssetsForAlbum: currentList);
-  }
-
-  void removeSelectedAdditionalAssets(List<Asset> assets) {
-    Set<Asset> currentList = state.selectedAdditionalAssetsForAlbum;
-
-    for (Asset asset in assets) {
-      currentList.removeWhere((e) => e.id == asset.id);
-    }
-
-    state = state.copyWith(selectedAdditionalAssetsForAlbum: currentList);
-  }
-
-  void removeAll() {
-    state = state.copyWith(
-      selectedNewAssetsForAlbum: {},
-      selectedMonths: {},
-      selectedAdditionalAssetsForAlbum: {},
-      selectedAssetsInAlbumViewer: {},
-      isAlbumExist: false,
-    );
-  }
-
-  void enableMultiselection() {
-    state = state.copyWith(isMultiselectEnable: true);
-  }
-
-  void disableMultiselection() {
-    state = state.copyWith(
-      isMultiselectEnable: false,
-      selectedAssetsInAlbumViewer: {},
-    );
-  }
-
-  void addAssetsInAlbumViewer(List<Asset> assets) {
-    state = state.copyWith(
-      selectedAssetsInAlbumViewer: {
-        ...state.selectedAssetsInAlbumViewer,
-        ...assets
-      },
-    );
-  }
-
-  void removeAssetsInAlbumViewer(List<Asset> assets) {
-    Set<Asset> currentList = state.selectedAssetsInAlbumViewer;
-
-    for (Asset asset in assets) {
-      currentList.removeWhere((e) => e.id == asset.id);
-    }
-
-    state = state.copyWith(selectedAssetsInAlbumViewer: currentList);
-  }
-}
-
-final assetSelectionProvider =
-    StateNotifierProvider<AssetSelectionNotifier, AssetSelectionState>((ref) {
-  return AssetSelectionNotifier();
-});
diff --git a/mobile/lib/modules/album/providers/shared_album.provider.dart b/mobile/lib/modules/album/providers/shared_album.provider.dart
index 24a57a993d..fd7b396dd4 100644
--- a/mobile/lib/modules/album/providers/shared_album.provider.dart
+++ b/mobile/lib/modules/album/providers/shared_album.provider.dart
@@ -1,7 +1,9 @@
-import 'package:collection/collection.dart';
+import 'dart:async';
+
 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/home/ui/asset_grid/asset_grid_data_structure.dart';
 import 'package:immich_mobile/shared/models/album.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
 import 'package:immich_mobile/shared/models/user.dart';
@@ -9,10 +11,14 @@ import 'package:immich_mobile/shared/providers/db.provider.dart';
 import 'package:isar/isar.dart';
 
 class SharedAlbumNotifier extends StateNotifier<List<Album>> {
-  SharedAlbumNotifier(this._albumService, this._db) : super([]);
+  SharedAlbumNotifier(this._albumService, Isar db) : super([]) {
+    final query = db.albums.filter().sharedEqualTo(true).sortByCreatedAtDesc();
+    query.findAll().then((value) => state = value);
+    _streamSub = query.watch().listen((data) => state = data);
+  }
 
   final AlbumService _albumService;
-  final Isar _db;
+  late final StreamSubscription<List<Album>> _streamSub;
 
   Future<Album?> createSharedAlbum(
     String albumName,
@@ -20,46 +26,21 @@ class SharedAlbumNotifier extends StateNotifier<List<Album>> {
     Iterable<User> sharedUsers,
   ) async {
     try {
-      final Album? newAlbum = await _albumService.createAlbum(
+      return await _albumService.createAlbum(
         albumName,
         assets,
         sharedUsers,
       );
-
-      if (newAlbum != null) {
-        state = [...state, newAlbum];
-        return newAlbum;
-      }
     } catch (e) {
       debugPrint("Error createSharedAlbum  ${e.toString()}");
     }
     return null;
   }
 
-  Future<void> getAllSharedAlbums() async {
-    var albums = await _db.albums
-        .filter()
-        .sharedEqualTo(true)
-        .sortByCreatedAtDesc()
-        .findAll();
-    if (!const ListEquality().equals(albums, state)) {
-      state = albums;
-    }
-    await _albumService.refreshRemoteAlbums(isShared: true);
-    albums = await _db.albums
-        .filter()
-        .sharedEqualTo(true)
-        .sortByCreatedAtDesc()
-        .findAll();
-    if (!const ListEquality().equals(albums, state)) {
-      state = albums;
-    }
-  }
+  Future<void> getAllSharedAlbums() =>
+      _albumService.refreshRemoteAlbums(isShared: true);
 
-  Future<bool> deleteAlbum(Album album) {
-    state = state.where((a) => a.id != album.id).toList();
-    return _albumService.deleteAlbum(album);
-  }
+  Future<bool> deleteAlbum(Album album) => _albumService.deleteAlbum(album);
 
   Future<bool> leaveAlbum(Album album) async {
     var res = await _albumService.leaveAlbum(album);
@@ -75,10 +56,16 @@ class SharedAlbumNotifier extends StateNotifier<List<Album>> {
   Future<bool> removeAssetFromAlbum(Album album, Iterable<Asset> assets) {
     return _albumService.removeAssetFromAlbum(album, assets);
   }
+
+  @override
+  void dispose() {
+    _streamSub.cancel();
+    super.dispose();
+  }
 }
 
 final sharedAlbumProvider =
-    StateNotifierProvider<SharedAlbumNotifier, List<Album>>((ref) {
+    StateNotifierProvider.autoDispose<SharedAlbumNotifier, List<Album>>((ref) {
   return SharedAlbumNotifier(
     ref.watch(albumServiceProvider),
     ref.watch(dbProvider),
@@ -86,10 +73,15 @@ final sharedAlbumProvider =
 });
 
 final sharedAlbumDetailProvider =
-    FutureProvider.autoDispose.family<Album?, int>((ref, albumId) async {
+    StreamProvider.autoDispose.family<Album, int>((ref, albumId) async* {
   final AlbumService sharedAlbumService = ref.watch(albumServiceProvider);
 
-  final Album? a = await sharedAlbumService.getAlbumDetail(albumId);
-  await a?.loadSortedAssets();
-  return a;
+  await for (final a in sharedAlbumService.watchAlbum(albumId)) {
+    if (a == null) {
+      throw Exception("Album with ID=$albumId does not exist anymore!");
+    }
+    await for (final _ in a.watchRenderList(GroupAssetsBy.none)) {
+      yield a;
+    }
+  }
 });
diff --git a/mobile/lib/modules/album/services/album.service.dart b/mobile/lib/modules/album/services/album.service.dart
index 4feabcf033..2e45d87c8d 100644
--- a/mobile/lib/modules/album/services/album.service.dart
+++ b/mobile/lib/modules/album/services/album.service.dart
@@ -214,8 +214,9 @@ class AlbumService {
     );
   }
 
-  Future<Album?> getAlbumDetail(int albumId) {
-    return _db.albums.get(albumId);
+  Stream<Album?> watchAlbum(int albumId) async* {
+    yield await _db.albums.get(albumId);
+    yield* _db.albums.watchObject(albumId);
   }
 
   Future<AddAssetsResponseDto?> addAdditionalAssetToAlbum(
diff --git a/mobile/lib/modules/album/ui/add_to_album_bottom_sheet.dart b/mobile/lib/modules/album/ui/add_to_album_bottom_sheet.dart
index 8834b9827f..3abe0d8078 100644
--- a/mobile/lib/modules/album/ui/add_to_album_bottom_sheet.dart
+++ b/mobile/lib/modules/album/ui/add_to_album_bottom_sheet.dart
@@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/modules/album/providers/album.provider.dart';
-import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
 import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
 import 'package:immich_mobile/modules/album/services/album.service.dart';
 import 'package:immich_mobile/modules/album/ui/add_to_album_sliverlist.dart';
@@ -110,12 +109,6 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
                               TextStyle(color: Theme.of(context).primaryColor),
                         ),
                         onPressed: () {
-                          ref
-                              .watch(assetSelectionProvider.notifier)
-                              .removeAll();
-                          ref
-                              .watch(assetSelectionProvider.notifier)
-                              .addNewAssets(assets);
                           AutoRouter.of(context).push(
                             CreateAlbumRoute(
                               isSharedAlbum: false,
diff --git a/mobile/lib/modules/album/ui/add_to_album_sliverlist.dart b/mobile/lib/modules/album/ui/add_to_album_sliverlist.dart
index 4fcd3beadb..d18740a9ad 100644
--- a/mobile/lib/modules/album/ui/add_to_album_sliverlist.dart
+++ b/mobile/lib/modules/album/ui/add_to_album_sliverlist.dart
@@ -9,12 +9,14 @@ class AddToAlbumSliverList extends HookConsumerWidget {
   final List<Album> albums;
   final List<Album> sharedAlbums;
   final void Function(Album) onAddToAlbum;
+  final bool enabled;
 
   const AddToAlbumSliverList({
     Key? key,
     required this.onAddToAlbum,
     required this.albums,
     required this.sharedAlbums,
+    this.enabled = true,
   }) : super(key: key);
 
   @override
@@ -28,14 +30,14 @@ class AddToAlbumSliverList extends HookConsumerWidget {
           return Padding(
             padding: const EdgeInsets.only(bottom: 8),
             child: ExpansionTile(
-              title:  Text('common_shared'.tr()),
+              title: Text('common_shared'.tr()),
               tilePadding: const EdgeInsets.symmetric(horizontal: 10.0),
               leading: const Icon(Icons.group),
               children: sharedAlbums
                   .map(
                     (album) => AlbumThumbnailListTile(
                       album: album,
-                      onTap: () => onAddToAlbum(album),
+                      onTap: enabled ? () => onAddToAlbum(album) : () {},
                     ),
                   )
                   .toList(),
@@ -48,7 +50,7 @@ class AddToAlbumSliverList extends HookConsumerWidget {
         final album = albums[offset];
         return AlbumThumbnailListTile(
           album: album,
-          onTap: () => onAddToAlbum(album),
+          onTap: enabled ? () => onAddToAlbum(album) : () {},
         );
       }),
     );
diff --git a/mobile/lib/modules/album/ui/album_viewer_appbar.dart b/mobile/lib/modules/album/ui/album_viewer_appbar.dart
index ec74e2fd13..91964b5cdf 100644
--- a/mobile/lib/modules/album/ui/album_viewer_appbar.dart
+++ b/mobile/lib/modules/album/ui/album_viewer_appbar.dart
@@ -5,10 +5,10 @@ import 'package:fluttertoast/fluttertoast.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/modules/album/providers/album.provider.dart';
 import 'package:immich_mobile/modules/album/providers/album_viewer.provider.dart';
-import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
 import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
 import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/shared/models/album.dart';
+import 'package:immich_mobile/shared/models/asset.dart';
 import 'package:immich_mobile/shared/ui/immich_toast.dart';
 import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
 
@@ -18,17 +18,19 @@ class AlbumViewerAppbar extends HookConsumerWidget
     Key? key,
     required this.album,
     required this.userId,
+    required this.selected,
+    required this.selectionDisabled,
+    required this.titleFocusNode,
   }) : super(key: key);
 
   final Album album;
   final String userId;
+  final Set<Asset> selected;
+  final void Function() selectionDisabled;
+  final FocusNode titleFocusNode;
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
-    final isMultiSelectionEnable =
-        ref.watch(assetSelectionProvider).isMultiselectEnable;
-    final selectedAssetsInAlbum =
-        ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
     final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText;
     final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum;
 
@@ -86,12 +88,12 @@ class AlbumViewerAppbar extends HookConsumerWidget
       bool isSuccess =
           await ref.watch(sharedAlbumProvider.notifier).removeAssetFromAlbum(
                 album,
-                selectedAssetsInAlbum,
+                selected,
               );
 
       if (isSuccess) {
         Navigator.pop(context);
-        ref.watch(assetSelectionProvider.notifier).disableMultiselection();
+        selectionDisabled();
         ref.watch(albumProvider.notifier).getAllAlbums();
         ref.invalidate(sharedAlbumDetailProvider(album.id));
       } else {
@@ -108,7 +110,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
     }
 
     buildBottomSheetActionButton() {
-      if (isMultiSelectionEnable) {
+      if (selected.isNotEmpty) {
         if (album.ownerId == userId) {
           return ListTile(
             leading: const Icon(Icons.delete_sweep_rounded),
@@ -163,11 +165,9 @@ class AlbumViewerAppbar extends HookConsumerWidget
     }
 
     buildLeadingButton() {
-      if (isMultiSelectionEnable) {
+      if (selected.isNotEmpty) {
         return IconButton(
-          onPressed: () => ref
-              .watch(assetSelectionProvider.notifier)
-              .disableMultiselection(),
+          onPressed: selectionDisabled,
           icon: const Icon(Icons.close_rounded),
           splashRadius: 25,
         );
@@ -202,9 +202,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
     return AppBar(
       elevation: 0,
       leading: buildLeadingButton(),
-      title: isMultiSelectionEnable
-          ? Text('${selectedAssetsInAlbum.length}')
-          : null,
+      title: selected.isNotEmpty ? Text('${selected.length}') : null,
       centerTitle: false,
       actions: [
         if (album.isRemote)
diff --git a/mobile/lib/modules/album/ui/album_viewer_thumbnail.dart b/mobile/lib/modules/album/ui/album_viewer_thumbnail.dart
deleted file mode 100644
index 8453de4994..0000000000
--- a/mobile/lib/modules/album/ui/album_viewer_thumbnail.dart
+++ /dev/null
@@ -1,163 +0,0 @@
-import 'package:auto_route/auto_route.dart';
-import 'package:flutter/material.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
-import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
-import 'package:immich_mobile/routing/router.dart';
-import 'package:immich_mobile/shared/models/asset.dart';
-import 'package:immich_mobile/shared/ui/immich_image.dart';
-import 'package:immich_mobile/utils/storage_indicator.dart';
-
-class AlbumViewerThumbnail extends HookConsumerWidget {
-  final Asset asset;
-  final List<Asset> assetList;
-  final bool showStorageIndicator;
-
-  const AlbumViewerThumbnail({
-    Key? key,
-    required this.asset,
-    required this.assetList,
-    this.showStorageIndicator = true,
-  }) : super(key: key);
-
-  @override
-  Widget build(BuildContext context, WidgetRef ref) {
-    final selectedAssetsInAlbumViewer =
-        ref.watch(assetSelectionProvider).selectedAssetsInAlbumViewer;
-    final isMultiSelectionEnable =
-        ref.watch(assetSelectionProvider).isMultiselectEnable;
-    final isFavorite = ref.watch(favoriteProvider).contains(asset.id);
-
-    viewAsset() {
-      AutoRouter.of(context).push(
-        GalleryViewerRoute(
-          asset: asset,
-          assetList: assetList,
-        ),
-      );
-    }
-
-    BoxBorder drawBorderColor() {
-      if (selectedAssetsInAlbumViewer.contains(asset)) {
-        return Border.all(
-          color: Theme.of(context).primaryColorLight,
-          width: 10,
-        );
-      } else {
-        return const Border();
-      }
-    }
-
-    enableMultiSelection() {
-      ref.watch(assetSelectionProvider.notifier).enableMultiselection();
-      ref
-          .watch(assetSelectionProvider.notifier)
-          .addAssetsInAlbumViewer([asset]);
-    }
-
-    disableMultiSelection() {
-      ref.watch(assetSelectionProvider.notifier).disableMultiselection();
-    }
-
-    buildVideoLabel() {
-      return Positioned(
-        top: 5,
-        right: 5,
-        child: Row(
-          children: [
-            Text(
-              asset.duration.toString().substring(0, 7),
-              style: const TextStyle(
-                color: Colors.white,
-                fontSize: 10,
-              ),
-            ),
-            const Icon(
-              Icons.play_circle_outline_rounded,
-              color: Colors.white,
-            ),
-          ],
-        ),
-      );
-    }
-
-    buildAssetStoreLocationIcon() {
-      return Positioned(
-        right: 10,
-        bottom: 5,
-        child: Icon(
-          storageIcon(asset),
-          color: Colors.white,
-          size: 18,
-        ),
-      );
-    }
-
-    buildAssetFavoriteIcon() {
-      return const Positioned(
-        left: 10,
-        bottom: 5,
-        child: Icon(
-          Icons.favorite,
-          color: Colors.white,
-          size: 18,
-        ),
-      );
-    }
-
-    buildAssetSelectionIcon() {
-      bool isSelected = selectedAssetsInAlbumViewer.contains(asset);
-
-      return Positioned(
-        left: 10,
-        top: 5,
-        child: isSelected
-            ? Icon(
-                Icons.check_circle_rounded,
-                color: Theme.of(context).primaryColor,
-              )
-            : const Icon(
-                Icons.check_circle_outline_rounded,
-                color: Colors.white,
-              ),
-      );
-    }
-
-    buildThumbnailImage() {
-      return Container(
-        decoration: BoxDecoration(border: drawBorderColor()),
-        child: ImmichImage(asset, width: 300, height: 300),
-      );
-    }
-
-    handleSelectionGesture() {
-      if (selectedAssetsInAlbumViewer.contains(asset)) {
-        ref
-            .watch(assetSelectionProvider.notifier)
-            .removeAssetsInAlbumViewer([asset]);
-
-        if (selectedAssetsInAlbumViewer.isEmpty) {
-          disableMultiSelection();
-        }
-      } else {
-        ref
-            .watch(assetSelectionProvider.notifier)
-            .addAssetsInAlbumViewer([asset]);
-      }
-    }
-
-    return GestureDetector(
-      onTap: isMultiSelectionEnable ? handleSelectionGesture : viewAsset,
-      onLongPress: enableMultiSelection,
-      child: Stack(
-        children: [
-          buildThumbnailImage(),
-          if (isFavorite) buildAssetFavoriteIcon(),
-          if (showStorageIndicator) buildAssetStoreLocationIcon(),
-          if (!asset.isImage) buildVideoLabel(),
-          if (isMultiSelectionEnable) buildAssetSelectionIcon(),
-        ],
-      ),
-    );
-  }
-}
diff --git a/mobile/lib/modules/album/ui/asset_grid_by_month.dart b/mobile/lib/modules/album/ui/asset_grid_by_month.dart
deleted file mode 100644
index 7dd523248b..0000000000
--- a/mobile/lib/modules/album/ui/asset_grid_by_month.dart
+++ /dev/null
@@ -1,26 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/modules/album/ui/selection_thumbnail_image.dart';
-import 'package:immich_mobile/shared/models/asset.dart';
-
-class AssetGridByMonth extends HookConsumerWidget {
-  final List<Asset> assetGroup;
-  const AssetGridByMonth({Key? key, required this.assetGroup})
-      : super(key: key);
-  @override
-  Widget build(BuildContext context, WidgetRef ref) {
-    return SliverGrid(
-      gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
-        crossAxisCount: 4,
-        crossAxisSpacing: 5.0,
-        mainAxisSpacing: 5,
-      ),
-      delegate: SliverChildBuilderDelegate(
-        (BuildContext context, int index) {
-          return SelectionThumbnailImage(asset: assetGroup[index]);
-        },
-        childCount: assetGroup.length,
-      ),
-    );
-  }
-}
diff --git a/mobile/lib/modules/album/ui/month_group_title.dart b/mobile/lib/modules/album/ui/month_group_title.dart
deleted file mode 100644
index 5d33d44ab0..0000000000
--- a/mobile/lib/modules/album/ui/month_group_title.dart
+++ /dev/null
@@ -1,117 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
-import 'package:immich_mobile/shared/models/asset.dart';
-
-class MonthGroupTitle extends HookConsumerWidget {
-  final String month;
-  final List<Asset> assetGroup;
-
-  const MonthGroupTitle({
-    Key? key,
-    required this.month,
-    required this.assetGroup,
-  }) : super(key: key);
-
-  @override
-  Widget build(BuildContext context, WidgetRef ref) {
-    final selectedDateGroup = ref.watch(assetSelectionProvider).selectedMonths;
-    final selectedAssets =
-        ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
-    final isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
-
-    handleTitleIconClick() {
-      HapticFeedback.heavyImpact();
-
-      if (isAlbumExist) {
-        if (selectedDateGroup.contains(month)) {
-          ref
-              .watch(assetSelectionProvider.notifier)
-              .removeAssetsInMonth(month, []);
-          ref
-              .watch(assetSelectionProvider.notifier)
-              .removeSelectedAdditionalAssets(assetGroup);
-        } else {
-          ref
-              .watch(assetSelectionProvider.notifier)
-              .addAllAssetsInMonth(month, []);
-
-          // Deep clone assetGroup
-          var assetGroupWithNewItems = [...assetGroup];
-
-          for (var selectedAsset in selectedAssets) {
-            assetGroupWithNewItems.removeWhere((a) => a.id == selectedAsset.id);
-          }
-
-          ref
-              .watch(assetSelectionProvider.notifier)
-              .addAdditionalAssets(assetGroupWithNewItems);
-        }
-      } else {
-        if (selectedDateGroup.contains(month)) {
-          ref
-              .watch(assetSelectionProvider.notifier)
-              .removeAssetsInMonth(month, assetGroup);
-        } else {
-          ref
-              .watch(assetSelectionProvider.notifier)
-              .addAllAssetsInMonth(month, assetGroup);
-        }
-      }
-    }
-
-    getSimplifiedMonth() {
-      var monthAndYear = month.split(',');
-      var yearText = monthAndYear[1].trim();
-      var monthText = monthAndYear[0].trim();
-      var currentYear = DateTime.now().year.toString();
-
-      if (yearText == currentYear) {
-        return monthText;
-      } else {
-        return month;
-      }
-    }
-
-    return SliverToBoxAdapter(
-      child: Padding(
-        padding: const EdgeInsets.only(
-          top: 29.0,
-          bottom: 29.0,
-          left: 14.0,
-          right: 8.0,
-        ),
-        child: Row(
-          children: [
-            GestureDetector(
-              onTap: handleTitleIconClick,
-              child: selectedDateGroup.contains(month)
-                  ? Icon(
-                      Icons.check_circle_rounded,
-                      color: Theme.of(context).primaryColor,
-                    )
-                  : const Icon(
-                      Icons.circle_outlined,
-                      color: Colors.grey,
-                    ),
-            ),
-            GestureDetector(
-              onTap: handleTitleIconClick,
-              child: Padding(
-                padding: const EdgeInsets.only(left: 8.0),
-                child: Text(
-                  getSimplifiedMonth(),
-                  style: TextStyle(
-                    fontSize: 24,
-                    color: Theme.of(context).primaryColor,
-                  ),
-                ),
-              ),
-            ),
-          ],
-        ),
-      ),
-    );
-  }
-}
diff --git a/mobile/lib/modules/album/ui/selection_thumbnail_image.dart b/mobile/lib/modules/album/ui/selection_thumbnail_image.dart
deleted file mode 100644
index 51cd2766c8..0000000000
--- a/mobile/lib/modules/album/ui/selection_thumbnail_image.dart
+++ /dev/null
@@ -1,141 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
-import 'package:immich_mobile/shared/models/asset.dart';
-import 'package:immich_mobile/shared/ui/immich_image.dart';
-
-class SelectionThumbnailImage extends HookConsumerWidget {
-  final Asset asset;
-
-  const SelectionThumbnailImage({Key? key, required this.asset})
-      : super(key: key);
-
-  @override
-  Widget build(BuildContext context, WidgetRef ref) {
-    var selectedAsset =
-        ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
-    var newAssetsForAlbum =
-        ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
-    var isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
-
-    Widget buildSelectionIcon(Asset asset) {
-      var isSelected = selectedAsset.map((item) => item.id).contains(asset.id);
-      var isNewlySelected =
-          newAssetsForAlbum.map((item) => item.id).contains(asset.id);
-
-      if (isSelected && !isAlbumExist) {
-        return Icon(
-          Icons.check_circle,
-          color: Theme.of(context).primaryColor,
-        );
-      } else if (isSelected && isAlbumExist) {
-        return const Icon(
-          Icons.check_circle,
-          color: Color.fromARGB(255, 233, 233, 233),
-        );
-      } else if (isNewlySelected && isAlbumExist) {
-        return Icon(
-          Icons.check_circle,
-          color: Theme.of(context).primaryColor,
-        );
-      } else {
-        return const Icon(
-          Icons.circle_outlined,
-          color: Colors.white,
-        );
-      }
-    }
-
-    BoxBorder drawBorderColor() {
-      var isSelected = selectedAsset.map((item) => item.id).contains(asset.id);
-      var isNewlySelected =
-          newAssetsForAlbum.map((item) => item.id).contains(asset.id);
-
-      if (isSelected && !isAlbumExist) {
-        return Border.all(
-          color: Theme.of(context).primaryColorLight,
-          width: 10,
-        );
-      } else if (isSelected && isAlbumExist) {
-        return Border.all(
-          color: const Color.fromARGB(255, 190, 190, 190),
-          width: 10,
-        );
-      } else if (isNewlySelected && isAlbumExist) {
-        return Border.all(
-          color: Theme.of(context).primaryColorLight,
-          width: 10,
-        );
-      }
-      return const Border();
-    }
-
-    return GestureDetector(
-      onTap: () {
-        var isSelected =
-            selectedAsset.map((item) => item.id).contains(asset.id);
-        var isNewlySelected =
-            newAssetsForAlbum.map((item) => item.id).contains(asset.id);
-
-        if (isAlbumExist) {
-          // Operation for existing album
-          if (!isSelected) {
-            if (isNewlySelected) {
-              ref
-                  .watch(assetSelectionProvider.notifier)
-                  .removeSelectedAdditionalAssets([asset]);
-            } else {
-              ref
-                  .watch(assetSelectionProvider.notifier)
-                  .addAdditionalAssets([asset]);
-            }
-          }
-        } else {
-          // Operation for new album
-          if (isSelected) {
-            ref
-                .watch(assetSelectionProvider.notifier)
-                .removeSelectedNewAssets([asset]);
-          } else {
-            ref.watch(assetSelectionProvider.notifier).addNewAssets([asset]);
-          }
-        }
-      },
-      child: Stack(
-        children: [
-          Container(
-            decoration: BoxDecoration(border: drawBorderColor()),
-            child: ImmichImage(asset, width: 150, height: 150),
-          ),
-          Padding(
-            padding: const EdgeInsets.all(3.0),
-            child: Align(
-              alignment: Alignment.topLeft,
-              child: buildSelectionIcon(asset),
-            ),
-          ),
-          if (!asset.isImage)
-            Positioned(
-              bottom: 5,
-              right: 5,
-              child: Row(
-                children: [
-                  Text(
-                    asset.duration.toString().substring(0, 7),
-                    style: const TextStyle(
-                      color: Colors.white,
-                      fontSize: 10,
-                    ),
-                  ),
-                  const Icon(
-                    Icons.play_circle_outline_rounded,
-                    color: Colors.white,
-                  ),
-                ],
-              ),
-            ),
-        ],
-      ),
-    );
-  }
-}
diff --git a/mobile/lib/modules/album/views/album_viewer_page.dart b/mobile/lib/modules/album/views/album_viewer_page.dart
index 748fcec8e4..b1adeb8219 100644
--- a/mobile/lib/modules/album/views/album_viewer_page.dart
+++ b/mobile/lib/modules/album/views/album_viewer_page.dart
@@ -5,21 +5,18 @@ import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/modules/album/providers/album.provider.dart';
-import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
-import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
 import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
-import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
-import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
+import 'package:immich_mobile/modules/album/providers/album.provider.dart';
 import 'package:immich_mobile/modules/album/services/album.service.dart';
 import 'package:immich_mobile/modules/album/ui/album_action_outlined_button.dart';
-import 'package:immich_mobile/modules/album/ui/album_viewer_appbar.dart';
 import 'package:immich_mobile/modules/album/ui/album_viewer_editable_title.dart';
-import 'package:immich_mobile/modules/album/ui/album_viewer_thumbnail.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/modules/home/ui/asset_grid/immich_asset_grid.dart';
+import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
+import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
+import 'package:immich_mobile/modules/album/ui/album_viewer_appbar.dart';
 import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/shared/models/album.dart';
+import 'package:immich_mobile/shared/models/asset.dart';
 import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 import 'package:immich_mobile/shared/ui/immich_sliver_persistent_app_bar_delegate.dart';
 import 'package:immich_mobile/shared/views/immich_loading_overlay.dart';
@@ -32,33 +29,51 @@ class AlbumViewerPage extends HookConsumerWidget {
   @override
   Widget build(BuildContext context, WidgetRef ref) {
     FocusNode titleFocusNode = useFocusNode();
-    ScrollController scrollController = useScrollController();
     final album = ref.watch(sharedAlbumDetailProvider(albumId));
-
     final userId = ref.watch(authenticationProvider).userId;
+    final selection = useState<Set<Asset>>({});
+    final multiSelectEnabled = useState(false);
+    bool? isTop;
+
+    Future<bool> onWillPop() async {
+      if (multiSelectEnabled.value) {
+        selection.value = {};
+        multiSelectEnabled.value = false;
+        return false;
+      }
+
+      return true;
+    }
+
+    void selectionListener(bool active, Set<Asset> selected) {
+      selection.value = selected;
+      multiSelectEnabled.value = selected.isNotEmpty;
+    }
+
+    void disableSelection() {
+      selection.value = {};
+      multiSelectEnabled.value = false;
+    }
 
     /// Find out if the assets in album exist on the device
     /// If they exist, add to selected asset state to show they are already selected.
     void onAddPhotosPressed(Album albumInfo) async {
-      if (albumInfo.assets.isNotEmpty == true) {
-        ref.watch(assetSelectionProvider.notifier).addNewAssets(
-              albumInfo.assets,
-            );
-      }
-
-      ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(true);
-
-      AssetSelectionPageResult? returnPayload = await AutoRouter.of(context)
-          .push<AssetSelectionPageResult?>(const AssetSelectionRoute());
+      AssetSelectionPageResult? returnPayload =
+          await AutoRouter.of(context).push<AssetSelectionPageResult?>(
+        AssetSelectionRoute(
+          existingAssets: albumInfo.assets,
+          isNewAlbum: false,
+        ),
+      );
 
       if (returnPayload != null) {
         // Check if there is new assets add
-        if (returnPayload.selectedAdditionalAsset.isNotEmpty) {
+        if (returnPayload.selectedAssets.isNotEmpty) {
           ImmichLoadingOverlayController.appLoader.show();
 
           var addAssetsResult =
               await ref.watch(albumServiceProvider).addAdditionalAssetToAlbum(
-                    returnPayload.selectedAdditionalAsset,
+                    returnPayload.selectedAssets,
                     albumInfo,
                   );
 
@@ -70,10 +85,6 @@ class AlbumViewerPage extends HookConsumerWidget {
 
           ImmichLoadingOverlayController.appLoader.hide();
         }
-
-        ref.watch(assetSelectionProvider.notifier).removeAll();
-      } else {
-        ref.watch(assetSelectionProvider.notifier).removeAll();
       }
     }
 
@@ -91,13 +102,38 @@ class AlbumViewerPage extends HookConsumerWidget {
             .addAdditionalUserToAlbum(sharedUserIds, album);
 
         if (isSuccess) {
-          ref.invalidate(sharedAlbumDetailProvider(albumId));
+          ref.invalidate(sharedAlbumDetailProvider(album.id));
         }
 
         ImmichLoadingOverlayController.appLoader.hide();
       }
     }
 
+    Widget buildControlButton(Album album) {
+      return Padding(
+        padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 8),
+        child: SizedBox(
+          height: 40,
+          child: ListView(
+            scrollDirection: Axis.horizontal,
+            children: [
+              AlbumActionOutlinedButton(
+                iconData: Icons.add_photo_alternate_outlined,
+                onPressed: () => onAddPhotosPressed(album),
+                labelText: "share_add_photos".tr(),
+              ),
+              if (userId == album.ownerId)
+                AlbumActionOutlinedButton(
+                  iconData: Icons.person_add_alt_rounded,
+                  onPressed: () => onAddUsersPressed(album),
+                  labelText: "album_viewer_page_share_add_users".tr(),
+                ),
+            ],
+          ),
+        ),
+      );
+    }
+
     Widget buildTitle(Album album) {
       return Padding(
         padding: const EdgeInsets.only(left: 8, right: 8, top: 16),
@@ -146,171 +182,104 @@ class AlbumViewerPage extends HookConsumerWidget {
     }
 
     Widget buildHeader(Album album) {
-      return SliverToBoxAdapter(
-        child: Column(
-          crossAxisAlignment: CrossAxisAlignment.start,
-          children: [
-            buildTitle(album),
-            if (album.assets.isNotEmpty == true) buildAlbumDateRange(album),
-            if (album.shared)
-              SizedBox(
-                height: 60,
-                child: ListView.builder(
-                  padding: const EdgeInsets.only(left: 16),
-                  scrollDirection: Axis.horizontal,
-                  itemBuilder: ((context, index) {
-                    return Padding(
-                      padding: const EdgeInsets.only(right: 8.0),
-                      child: CircleAvatar(
-                        backgroundColor: Colors.grey[300],
-                        radius: 18,
-                        child: Padding(
-                          padding: const EdgeInsets.all(2.0),
-                          child: ClipRRect(
-                            borderRadius: BorderRadius.circular(50.0),
-                            child: Image.asset(
-                              'assets/immich-logo-no-outline.png',
-                            ),
+      return Column(
+        mainAxisAlignment: MainAxisAlignment.end,
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          buildTitle(album),
+          if (album.assets.isNotEmpty == true) buildAlbumDateRange(album),
+          if (album.shared)
+            SizedBox(
+              height: 50,
+              child: ListView.builder(
+                padding: const EdgeInsets.only(left: 16),
+                scrollDirection: Axis.horizontal,
+                itemBuilder: ((context, index) {
+                  return Padding(
+                    padding: const EdgeInsets.only(right: 8.0),
+                    child: CircleAvatar(
+                      backgroundColor: Colors.grey[300],
+                      radius: 18,
+                      child: Padding(
+                        padding: const EdgeInsets.all(2.0),
+                        child: ClipRRect(
+                          borderRadius: BorderRadius.circular(50.0),
+                          child: Image.asset(
+                            'assets/immich-logo-no-outline.png',
                           ),
                         ),
                       ),
-                    );
-                  }),
-                  itemCount: album.sharedUsers.length,
-                ),
-              )
-          ],
-        ),
-      );
-    }
-
-    Widget buildImageGrid(Album album) {
-      final appSettingService = ref.watch(appSettingsServiceProvider);
-      final bool showStorageIndicator =
-          appSettingService.getSetting(AppSettingsEnum.storageIndicator);
-
-      if (album.sortedAssets.isNotEmpty) {
-        return SliverPadding(
-          padding: const EdgeInsets.only(top: 10.0),
-          sliver: SliverGrid(
-            gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
-              crossAxisCount:
-                  appSettingService.getSetting(AppSettingsEnum.tilesPerRow),
-              crossAxisSpacing: 5.0,
-              mainAxisSpacing: 5,
-            ),
-            delegate: SliverChildBuilderDelegate(
-              (BuildContext context, int index) {
-                return AlbumViewerThumbnail(
-                  asset: album.sortedAssets[index],
-                  assetList: album.sortedAssets,
-                  showStorageIndicator: showStorageIndicator,
-                );
-              },
-              childCount: album.assetCount,
-            ),
-          ),
-        );
-      }
-      return const SliverToBoxAdapter();
-    }
-
-    Widget buildControlButton(Album album) {
-      return Padding(
-        padding: const EdgeInsets.only(left: 16.0, top: 8, bottom: 8),
-        child: SizedBox(
-          height: 40,
-          child: ListView(
-            scrollDirection: Axis.horizontal,
-            children: [
-              AlbumActionOutlinedButton(
-                iconData: Icons.add_photo_alternate_outlined,
-                onPressed: () => onAddPhotosPressed(album),
-                labelText: "share_add_photos".tr(),
-              ),
-              if (userId == album.ownerId)
-                AlbumActionOutlinedButton(
-                  iconData: Icons.person_add_alt_rounded,
-                  onPressed: () => onAddUsersPressed(album),
-                  labelText: "album_viewer_page_share_add_users".tr(),
-                ),
-            ],
-          ),
-        ),
-      );
-    }
-
-    Future<bool> onWillPop() async {
-      final isMultiselectEnable = ref
-          .read(assetSelectionProvider)
-          .selectedAssetsInAlbumViewer
-          .isNotEmpty;
-      if (isMultiselectEnable) {
-        ref.watch(assetSelectionProvider.notifier).removeAll();
-        return false;
-      }
-
-      return true;
-    }
-
-    Widget buildBody(Album album) {
-      return WillPopScope(
-        onWillPop: onWillPop,
-        child: GestureDetector(
-          onTap: () {
-            titleFocusNode.unfocus();
-          },
-          child: DraggableScrollbar.semicircle(
-            backgroundColor: Theme.of(context).hintColor,
-            controller: scrollController,
-            heightScrollThumb: 48.0,
-            child: CustomScrollView(
-              controller: scrollController,
-              slivers: [
-                buildHeader(album),
-                if (album.isRemote)
-                  SliverPersistentHeader(
-                    pinned: true,
-                    delegate: ImmichSliverPersistentAppBarDelegate(
-                      minHeight: 50,
-                      maxHeight: 50,
-                      child: Container(
-                        color: Theme.of(context).scaffoldBackgroundColor,
-                        child: buildControlButton(album),
-                      ),
                     ),
-                  ),
-                SliverSafeArea(
-                  sliver: buildImageGrid(album),
-                ),
-              ],
+                  );
+                }),
+                itemCount: album.sharedUsers.length,
+              ),
             ),
-          ),
-        ),
+        ],
       );
     }
 
+    final scroll = ScrollController();
+
     return Scaffold(
       appBar: album.when(
-        data: (Album? data) {
-          if (data != null) {
-            return AlbumViewerAppbar(
-              album: data,
-              userId: userId,
-            );
-          }
-          return null;
-        },
-        error: (e, _) => null,
-        loading: () => null,
+        data: (data) => AlbumViewerAppbar(
+          titleFocusNode: titleFocusNode,
+          album: data,
+          userId: userId,
+          selected: selection.value,
+          selectionDisabled: disableSelection,
+        ),
+        error: (error, stackTrace) => AppBar(title: const Text("Error")),
+        loading: () => AppBar(),
       ),
       body: album.when(
-        data: (albumInfo) => albumInfo != null
-            ? buildBody(albumInfo)
-            : const Center(
-                child: CircularProgressIndicator(),
+        data: (data) => WillPopScope(
+          onWillPop: onWillPop,
+          child: GestureDetector(
+            onTap: () {
+              titleFocusNode.unfocus();
+            },
+            child: NestedScrollView(
+              controller: scroll,
+              floatHeaderSlivers: true,
+              headerSliverBuilder: (context, innerBoxIsScrolled) => [
+                SliverToBoxAdapter(child: buildHeader(data)),
+                SliverPersistentHeader(
+                  pinned: true,
+                  delegate: ImmichSliverPersistentAppBarDelegate(
+                    minHeight: 50,
+                    maxHeight: 50,
+                    child: Container(
+                      color: Theme.of(context).scaffoldBackgroundColor,
+                      child: buildControlButton(data),
+                    ),
+                  ),
+                )
+              ],
+              body: ImmichAssetGrid(
+                renderList: data.renderList,
+                listener: selectionListener,
+                selectionActive: multiSelectEnabled.value,
+                showMultiSelectIndicator: false,
+                visibleItemsListener: (start, end) {
+                  final top = start.index == 0 && start.itemLeadingEdge == 0.0;
+                  if (top != isTop) {
+                    isTop = top;
+                    scroll.animateTo(
+                      top
+                          ? scroll.position.minScrollExtent
+                          : scroll.position.maxScrollExtent,
+                      duration: const Duration(milliseconds: 500),
+                      curve: top ? Curves.easeOut : Curves.easeIn,
+                    );
+                  }
+                },
               ),
-        error: (e, _) => Center(child: Text("Error loading album info $e")),
+            ),
+          ),
+        ),
+        error: (e, _) => Center(child: Text("Error loading album info!\n$e")),
         loading: () => const Center(
           child: ImmichLoadingIndicator(),
         ),
diff --git a/mobile/lib/modules/album/views/asset_selection_page.dart b/mobile/lib/modules/album/views/asset_selection_page.dart
index 3654545179..74aac07405 100644
--- a/mobile/lib/modules/album/views/asset_selection_page.dart
+++ b/mobile/lib/modules/album/views/asset_selection_page.dart
@@ -4,54 +4,42 @@ import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
-import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
-import 'package:immich_mobile/modules/album/ui/asset_grid_by_month.dart';
-import 'package:immich_mobile/modules/album/ui/month_group_title.dart';
+import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
+import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
+import 'package:immich_mobile/shared/models/asset.dart';
 import 'package:immich_mobile/shared/providers/asset.provider.dart';
-import 'package:immich_mobile/modules/home/ui/draggable_scrollbar.dart';
 
 class AssetSelectionPage extends HookConsumerWidget {
-  const AssetSelectionPage({Key? key}) : super(key: key);
+  const AssetSelectionPage({
+    Key? key,
+    required this.existingAssets,
+    this.isNewAlbum = false,
+  }) : super(key: key);
+
+  final Set<Asset> existingAssets;
+  final bool isNewAlbum;
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
-    ScrollController scrollController = useScrollController();
-    var assetGroupMonthYear = ref.watch(assetGroupByMonthYearProvider);
-    final selectedAssets =
-        ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
-    final newAssetsForAlbum =
-        ref.watch(assetSelectionProvider).selectedAdditionalAssetsForAlbum;
-    final isAlbumExist = ref.watch(assetSelectionProvider).isAlbumExist;
-
-    List<Widget> imageGridGroup = [];
+    final renderList = ref.watch(remoteAssetsProvider);
+    final selected = useState<Set<Asset>>(existingAssets);
+    final selectionEnabledHook = useState(true);
 
     String buildAssetCountText() {
-      if (isAlbumExist) {
-        return (selectedAssets.length + newAssetsForAlbum.length).toString();
-      } else {
-        return selectedAssets.length.toString();
-      }
+      return selected.value.length.toString();
     }
 
-    Widget buildBody() {
-      assetGroupMonthYear.forEach((monthYear, assetGroup) {
-        imageGridGroup
-            .add(MonthGroupTitle(month: monthYear, assetGroup: assetGroup));
-        imageGridGroup.add(AssetGridByMonth(assetGroup: assetGroup));
-      });
-
-      return Stack(
-        children: [
-          DraggableScrollbar.semicircle(
-            backgroundColor: Theme.of(context).hintColor,
-            controller: scrollController,
-            heightScrollThumb: 48.0,
-            child: CustomScrollView(
-              controller: scrollController,
-              slivers: [...imageGridGroup],
-            ),
-          ),
-        ],
+    Widget buildBody(RenderList renderList) {
+      return ImmichAssetGrid(
+        renderList: renderList,
+        listener: (active, assets) {
+          selectionEnabledHook.value = active;
+          selected.value = assets;
+        },
+        selectionActive: true,
+        preselectedAssets: isNewAlbum ? selected.value : existingAssets,
+        canDeselect: isNewAlbum,
+        showMultiSelectIndicator: false,
       );
     }
 
@@ -61,11 +49,10 @@ class AssetSelectionPage extends HookConsumerWidget {
         leading: IconButton(
           icon: const Icon(Icons.close_rounded),
           onPressed: () {
-            ref.watch(assetSelectionProvider.notifier).removeAll();
-            AutoRouter.of(context).pop(null);
+            AutoRouter.of(context).popForced(null);
           },
         ),
-        title: selectedAssets.isEmpty
+        title: selected.value.isEmpty
             ? const Text(
                 'share_add_photos',
                 style: TextStyle(fontSize: 18),
@@ -76,16 +63,13 @@ class AssetSelectionPage extends HookConsumerWidget {
               ),
         centerTitle: false,
         actions: [
-          if ((!isAlbumExist && selectedAssets.isNotEmpty) ||
-              (isAlbumExist && newAssetsForAlbum.isNotEmpty))
+          if (selected.value.isNotEmpty)
             TextButton(
               onPressed: () {
-                var payload = AssetSelectionPageResult(
-                  isAlbumExist: isAlbumExist,
-                  selectedAdditionalAsset: newAssetsForAlbum,
-                  selectedNewAsset: selectedAssets,
-                );
-                AutoRouter.of(context).pop(payload);
+                var payload =
+                    AssetSelectionPageResult(selectedAssets: selected.value);
+                AutoRouter.of(context)
+                    .popForced<AssetSelectionPageResult>(payload);
               },
               child: const Text(
                 "share_add",
@@ -94,7 +78,13 @@ class AssetSelectionPage extends HookConsumerWidget {
             ),
         ],
       ),
-      body: buildBody(),
+      body: renderList.when(
+        data: (data) => buildBody(data),
+        error: (error, stackTrace) => Center(
+          child: Text(error.toString()),
+        ),
+        loading: () => const Center(child: CircularProgressIndicator()),
+      ),
     );
   }
 }
diff --git a/mobile/lib/modules/album/views/create_album_page.dart b/mobile/lib/modules/album/views/create_album_page.dart
index 0b6e19b530..c799eb3f17 100644
--- a/mobile/lib/modules/album/views/create_album_page.dart
+++ b/mobile/lib/modules/album/views/create_album_page.dart
@@ -6,7 +6,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
 import 'package:immich_mobile/modules/album/providers/album.provider.dart';
 import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
-import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
 import 'package:immich_mobile/modules/album/ui/album_action_outlined_button.dart';
 import 'package:immich_mobile/modules/album/ui/album_title_text_field.dart';
 import 'package:immich_mobile/modules/album/ui/shared_album_thumbnail_image.dart';
@@ -31,12 +30,15 @@ class CreateAlbumPage extends HookConsumerWidget {
     final albumTitleTextFieldFocusNode = useFocusNode();
     final isAlbumTitleTextFieldFocus = useState(false);
     final isAlbumTitleEmpty = useState(true);
-    final selectedAssets =
-        ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum;
+    final selectedAssets = useState<Set<Asset>>(const {});
     final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
 
-    showSelectUserPage() {
-      AutoRouter.of(context).push(const SelectUserForSharingRoute());
+    showSelectUserPage() async {
+      final bool? ok = await AutoRouter.of(context)
+          .push<bool?>(SelectUserForSharingRoute(assets: selectedAssets.value));
+      if (ok == true) {
+        selectedAssets.value = {};
+      }
     }
 
     void onBackgroundTapped() {
@@ -52,13 +54,17 @@ class CreateAlbumPage extends HookConsumerWidget {
     }
 
     onSelectPhotosButtonPressed() async {
-      ref.watch(assetSelectionProvider.notifier).setIsAlbumExist(false);
-
-      AssetSelectionPageResult? selectedAsset = await AutoRouter.of(context)
-          .push<AssetSelectionPageResult?>(const AssetSelectionRoute());
-
+      AssetSelectionPageResult? selectedAsset =
+          await AutoRouter.of(context).push<AssetSelectionPageResult?>(
+        AssetSelectionRoute(
+          existingAssets: selectedAssets.value,
+          isNewAlbum: true,
+        ),
+      );
       if (selectedAsset == null) {
-        ref.watch(assetSelectionProvider.notifier).removeAll();
+        selectedAssets.value = const {};
+      } else {
+        selectedAssets.value = selectedAsset.selectedAssets;
       }
     }
 
@@ -78,7 +84,7 @@ class CreateAlbumPage extends HookConsumerWidget {
     }
 
     buildTitle() {
-      if (selectedAssets.isEmpty) {
+      if (selectedAssets.value.isEmpty) {
         return SliverToBoxAdapter(
           child: Padding(
             padding: const EdgeInsets.only(top: 200, left: 18),
@@ -97,7 +103,7 @@ class CreateAlbumPage extends HookConsumerWidget {
     }
 
     buildSelectPhotosButton() {
-      if (selectedAssets.isEmpty) {
+      if (selectedAssets.value.isEmpty) {
         return SliverToBoxAdapter(
           child: Padding(
             padding: const EdgeInsets.only(top: 16, left: 18, right: 18),
@@ -158,7 +164,7 @@ class CreateAlbumPage extends HookConsumerWidget {
     }
 
     buildSelectedImageGrid() {
-      if (selectedAssets.isNotEmpty) {
+      if (selectedAssets.value.isNotEmpty) {
         return SliverPadding(
           padding: const EdgeInsets.only(top: 16),
           sliver: SliverGrid(
@@ -172,11 +178,11 @@ class CreateAlbumPage extends HookConsumerWidget {
                 return GestureDetector(
                   onTap: onBackgroundTapped,
                   child: SharedAlbumThumbnailImage(
-                    asset: selectedAssets.elementAt(index),
+                    asset: selectedAssets.value.elementAt(index),
                   ),
                 );
               },
-              childCount: selectedAssets.length,
+              childCount: selectedAssets.value.length,
             ),
           ),
         );
@@ -188,12 +194,12 @@ class CreateAlbumPage extends HookConsumerWidget {
     createNonSharedAlbum() async {
       var newAlbum = await ref.watch(albumProvider.notifier).createAlbum(
             ref.watch(albumTitleProvider),
-            ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum,
+            selectedAssets.value,
           );
 
       if (newAlbum != null) {
         ref.watch(albumProvider.notifier).getAllAlbums();
-        ref.watch(assetSelectionProvider.notifier).removeAll();
+        selectedAssets.value = {};
         ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
 
         AutoRouter.of(context).replace(AlbumViewerRoute(albumId: newAlbum.id));
@@ -207,7 +213,7 @@ class CreateAlbumPage extends HookConsumerWidget {
         backgroundColor: Theme.of(context).scaffoldBackgroundColor,
         leading: IconButton(
           onPressed: () {
-            ref.watch(assetSelectionProvider.notifier).removeAll();
+            selectedAssets.value = {};
             AutoRouter.of(context).pop();
           },
           icon: const Icon(Icons.close_rounded),
@@ -237,7 +243,7 @@ class CreateAlbumPage extends HookConsumerWidget {
           if (!isSharedAlbum)
             TextButton(
               onPressed: albumTitleController.text.isNotEmpty &&
-                      selectedAssets.isNotEmpty
+                      selectedAssets.value.isNotEmpty
                   ? createNonSharedAlbum
                   : null,
               child: Text(
@@ -264,7 +270,7 @@ class CreateAlbumPage extends HookConsumerWidget {
                 child: Column(
                   children: [
                     buildTitleInputField(),
-                    if (selectedAssets.isNotEmpty) buildControlButton(),
+                    if (selectedAssets.value.isNotEmpty) buildControlButton(),
                   ],
                 ),
               ),
diff --git a/mobile/lib/modules/album/views/select_user_for_sharing_page.dart b/mobile/lib/modules/album/views/select_user_for_sharing_page.dart
index 976a781e29..4130181c74 100644
--- a/mobile/lib/modules/album/views/select_user_for_sharing_page.dart
+++ b/mobile/lib/modules/album/views/select_user_for_sharing_page.dart
@@ -4,15 +4,18 @@ import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/modules/album/providers/album_title.provider.dart';
-import 'package:immich_mobile/modules/album/providers/asset_selection.provider.dart';
 import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
 import 'package:immich_mobile/modules/album/providers/suggested_shared_users.provider.dart';
 import 'package:immich_mobile/routing/router.dart';
+import 'package:immich_mobile/shared/models/asset.dart';
 import 'package:immich_mobile/shared/models/user.dart';
 import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 
 class SelectUserForSharingPage extends HookConsumerWidget {
-  const SelectUserForSharingPage({Key? key}) : super(key: key);
+  const SelectUserForSharingPage({Key? key, required this.assets})
+      : super(key: key);
+
+  final Set<Asset> assets;
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
@@ -24,15 +27,15 @@ class SelectUserForSharingPage extends HookConsumerWidget {
       var newAlbum =
           await ref.watch(sharedAlbumProvider.notifier).createSharedAlbum(
                 ref.watch(albumTitleProvider),
-                ref.watch(assetSelectionProvider).selectedNewAssetsForAlbum,
+                assets,
                 sharedUsersList.value,
               );
 
       if (newAlbum != null) {
         await ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
-        ref.watch(assetSelectionProvider.notifier).removeAll();
+        // ref.watch(assetSelectionProvider.notifier).removeAll();
         ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
-
+        AutoRouter.of(context).pop(true);
         AutoRouter.of(context)
             .navigate(const TabControllerRoute(children: [SharingRoute()]));
       }
diff --git a/mobile/lib/modules/archive/providers/archive_asset_provider.dart b/mobile/lib/modules/archive/providers/archive_asset_provider.dart
index 28d8d74f65..70c8ac89e8 100644
--- a/mobile/lib/modules/archive/providers/archive_asset_provider.dart
+++ b/mobile/lib/modules/archive/providers/archive_asset_provider.dart
@@ -1,55 +1,25 @@
 import 'package:hooks_riverpod/hooks_riverpod.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/providers/asset.provider.dart';
+import 'package:immich_mobile/shared/models/store.dart';
 import 'package:immich_mobile/shared/providers/db.provider.dart';
 import 'package:isar/isar.dart';
 
-class ArchiveSelectionNotifier extends StateNotifier<Set<int>> {
-  ArchiveSelectionNotifier(this.db, this.assetNotifier) : super({}) {
-    state = db.assets
-        .filter()
-        .isArchivedEqualTo(true)
-        .findAllSync()
-        .map((e) => e.id)
-        .toSet();
+final archiveProvider = StreamProvider<RenderList>((ref) async* {
+  final query = ref
+      .watch(dbProvider)
+      .assets
+      .filter()
+      .ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
+      .isArchivedEqualTo(true)
+      .sortByFileCreatedAt();
+  final settings = ref.watch(appSettingsServiceProvider);
+  final groupBy =
+      GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)];
+  yield await RenderList.fromQuery(query, groupBy);
+  await for (final _ in query.watchLazy()) {
+    yield await RenderList.fromQuery(query, groupBy);
   }
-
-  final Isar db;
-  final AssetNotifier assetNotifier;
-
-  void _setArchiveForAssetId(int id, bool archive) {
-    if (!archive) {
-      state = state.difference({id});
-    } else {
-      state = state.union({id});
-    }
-  }
-
-  bool _isArchive(int id) {
-    return state.contains(id);
-  }
-
-  Future<void> toggleArchive(Asset asset) async {
-    if (asset.storage == AssetState.local) return;
-
-    _setArchiveForAssetId(asset.id, !_isArchive(asset.id));
-
-    await assetNotifier.toggleArchive(
-      [asset],
-      state.contains(asset.id),
-    );
-  }
-
-  Future<void> addToArchives(Iterable<Asset> assets) {
-    state = state.union(assets.map((a) => a.id).toSet());
-    return assetNotifier.toggleArchive(assets, true);
-  }
-}
-
-final archiveProvider =
-    StateNotifierProvider<ArchiveSelectionNotifier, Set<int>>((ref) {
-  return ArchiveSelectionNotifier(
-    ref.watch(dbProvider),
-    ref.watch(assetProvider.notifier),
-  );
 });
diff --git a/mobile/lib/modules/archive/providers/store_providers_here.txt b/mobile/lib/modules/archive/providers/store_providers_here.txt
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/mobile/lib/modules/archive/views/archive_page.dart b/mobile/lib/modules/archive/views/archive_page.dart
index b884a41009..f5ca80406b 100644
--- a/mobile/lib/modules/archive/views/archive_page.dart
+++ b/mobile/lib/modules/archive/views/archive_page.dart
@@ -1,46 +1,25 @@
 import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
+import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/modules/archive/providers/archive_asset_provider.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
-import 'package:immich_mobile/shared/models/store.dart';
-import 'package:immich_mobile/shared/models/user.dart';
 import 'package:immich_mobile/shared/providers/asset.provider.dart';
-import 'package:immich_mobile/shared/providers/db.provider.dart';
+import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 import 'package:immich_mobile/shared/ui/immich_toast.dart';
-import 'package:isar/isar.dart';
 
 class ArchivePage extends HookConsumerWidget {
   const ArchivePage({super.key});
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
-    final User me = Store.get(StoreKey.currentUser);
-    final query = ref
-        .watch(dbProvider)
-        .assets
-        .filter()
-        .ownerIdEqualTo(me.isarId)
-        .isArchivedEqualTo(true);
-    final stream = query.watch();
-    final archivedAssets = useState<List<Asset>>([]);
+    final archivedAssets = ref.watch(archiveProvider);
     final selectionEnabledHook = useState(false);
     final selection = useState(<Asset>{});
-
-    useEffect(
-      () {
-        query.findAll().then((value) => archivedAssets.value = value);
-        final subscription = stream.listen((e) {
-          archivedAssets.value = e;
-        });
-        // Cancel the subscription when the widget is disposed
-        return subscription.cancel;
-      },
-      [],
-    );
+    final processing = useState(false);
 
     void selectionListener(
       bool multiselect,
@@ -50,7 +29,7 @@ class ArchivePage extends HookConsumerWidget {
       selection.value = selectedAssets;
     }
 
-    AppBar buildAppBar() {
+    AppBar buildAppBar(String count) {
       return AppBar(
         leading: IconButton(
           onPressed: () => AutoRouter.of(context).pop(),
@@ -60,7 +39,7 @@ class ArchivePage extends HookConsumerWidget {
         automaticallyImplyLeading: false,
         title: const Text(
           'archive_page_title',
-        ).tr(args: [archivedAssets.value.length.toString()]),
+        ).tr(args: [count]),
       );
     }
 
@@ -84,24 +63,34 @@ class ArchivePage extends HookConsumerWidget {
                       'control_bottom_app_bar_unarchive'.tr(),
                       style: const TextStyle(fontSize: 14),
                     ),
-                    onTap: () {
-                      if (selection.value.isNotEmpty) {
-                        ref
-                            .watch(assetProvider.notifier)
-                            .toggleArchive(selection.value, false);
+                    onTap: processing.value
+                        ? null
+                        : () async {
+                            processing.value = true;
+                            try {
+                              if (selection.value.isNotEmpty) {
+                                await ref
+                                    .watch(assetProvider.notifier)
+                                    .toggleArchive(
+                                      selection.value.toList(),
+                                      false,
+                                    );
 
-                        final assetOrAssets =
-                            selection.value.length > 1 ? 'assets' : 'asset';
-                        ImmichToast.show(
-                          context: context,
-                          msg:
-                              'Moved ${selection.value.length} $assetOrAssets to library',
-                          gravity: ToastGravity.CENTER,
-                        );
-                      }
-
-                      selectionEnabledHook.value = false;
-                    },
+                                final assetOrAssets = selection.value.length > 1
+                                    ? 'assets'
+                                    : 'asset';
+                                ImmichToast.show(
+                                  context: context,
+                                  msg:
+                                      'Moved ${selection.value.length} $assetOrAssets to library',
+                                  gravity: ToastGravity.CENTER,
+                                );
+                              }
+                            } finally {
+                              processing.value = false;
+                              selectionEnabledHook.value = false;
+                            }
+                          },
                   )
                 ],
               ),
@@ -111,22 +100,34 @@ class ArchivePage extends HookConsumerWidget {
       );
     }
 
-    return Scaffold(
-      appBar: buildAppBar(),
-      body: archivedAssets.value.isEmpty
-          ? Center(
-              child: Text('archive_page_no_archived_assets'.tr()),
-            )
-          : Stack(
-              children: [
-                ImmichAssetGrid(
-                  assets: archivedAssets.value,
-                  listener: selectionListener,
-                  selectionActive: selectionEnabledHook.value,
-                ),
-                if (selectionEnabledHook.value) buildBottomBar()
-              ],
-            ),
+    return archivedAssets.when(
+      loading: () => Scaffold(
+        appBar: buildAppBar("?"),
+        body: const Center(child: CircularProgressIndicator()),
+      ),
+      error: (error, stackTrace) => Scaffold(
+        appBar: buildAppBar("Error"),
+        body: Center(child: Text(error.toString())),
+      ),
+      data: (data) => Scaffold(
+        appBar: buildAppBar(data.totalAssets.toString()),
+        body: data.isEmpty
+            ? Center(
+                child: Text('archive_page_no_archived_assets'.tr()),
+              )
+            : Stack(
+                children: [
+                  ImmichAssetGrid(
+                    renderList: data,
+                    listener: selectionListener,
+                    selectionActive: selectionEnabledHook.value,
+                  ),
+                  if (selectionEnabledHook.value) buildBottomBar(),
+                  if (processing.value)
+                    const Center(child: ImmichLoadingIndicator())
+                ],
+              ),
+      ),
     );
   }
 }
diff --git a/mobile/lib/modules/asset_viewer/models/request_download_asset_info.model.dart b/mobile/lib/modules/asset_viewer/models/request_download_asset_info.model.dart
deleted file mode 100644
index 80a99bfe9c..0000000000
--- a/mobile/lib/modules/asset_viewer/models/request_download_asset_info.model.dart
+++ /dev/null
@@ -1,6 +0,0 @@
-class RequestDownloadAssetInfo {
-  final String assetId;
-  final String deviceId;
-
-  RequestDownloadAssetInfo(this.assetId, this.deviceId);
-}
diff --git a/mobile/lib/modules/asset_viewer/providers/render_list.provider.dart b/mobile/lib/modules/asset_viewer/providers/render_list.provider.dart
index e106e97ed1..2273380843 100644
--- a/mobile/lib/modules/asset_viewer/providers/render_list.provider.dart
+++ b/mobile/lib/modules/asset_viewer/providers/render_list.provider.dart
@@ -4,14 +4,12 @@ import 'package:immich_mobile/modules/settings/providers/app_settings.provider.d
 import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
 
-final renderListProvider = FutureProvider.family<RenderList, List<Asset>>((ref, assets) {
-  var settings = ref.watch(appSettingsServiceProvider);
+final renderListProvider =
+    FutureProvider.family<RenderList, List<Asset>>((ref, assets) {
+  final settings = ref.watch(appSettingsServiceProvider);
 
-  final layout = AssetGridLayoutParameters(
-    settings.getSetting(AppSettingsEnum.tilesPerRow),
-    settings.getSetting(AppSettingsEnum.dynamicLayout),
+  return RenderList.fromAssets(
+    assets,
     GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)],
   );
-
-  return RenderList.fromAssets(assets, layout);
 });
diff --git a/mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart b/mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart
index 436a5bb28e..b79f240bba 100644
--- a/mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart
+++ b/mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart
@@ -21,7 +21,7 @@ class TopControlAppBar extends HookConsumerWidget {
   final VoidCallback? onDownloadPressed;
   final VoidCallback onToggleMotionVideo;
   final VoidCallback onAddToAlbumPressed;
-  final VoidCallback onFavorite;
+  final VoidCallback? onFavorite;
   final bool isPlayingMotionVideo;
   final bool isFavorite;
 
@@ -31,9 +31,7 @@ class TopControlAppBar extends HookConsumerWidget {
 
     Widget buildFavoriteButton() {
       return IconButton(
-        onPressed: () {
-          onFavorite();
-        },
+        onPressed: onFavorite,
         icon: Icon(
           isFavorite ? Icons.favorite : Icons.favorite_border,
           color: Colors.grey[200],
diff --git a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart
index b5ad98ddfc..29e932b354 100644
--- a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart
+++ b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart
@@ -12,9 +12,7 @@ import 'package:immich_mobile/modules/asset_viewer/ui/advanced_bottom_sheet.dart
 import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart';
 import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart';
 import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
-import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
 import 'package:immich_mobile/shared/models/store.dart';
-import 'package:immich_mobile/shared/services/asset.service.dart';
 import 'package:immich_mobile/modules/home/ui/delete_dialog.dart';
 import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
 import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
@@ -32,16 +30,16 @@ import 'package:openapi/api.dart' as api;
 
 // ignore: must_be_immutable
 class GalleryViewerPage extends HookConsumerWidget {
-  final List<Asset> assetList;
-  final Asset asset;
+  final Asset Function(int index) loadAsset;
+  final int totalAssets;
+  final int initialIndex;
 
   GalleryViewerPage({
     super.key,
-    required this.assetList,
-    required this.asset,
-  }) : controller = PageController(initialPage: assetList.indexOf(asset));
-
-  Asset? assetDetail;
+    required this.initialIndex,
+    required this.loadAsset,
+    required this.totalAssets,
+  }) : controller = PageController(initialPage: initialIndex);
 
   final PageController controller;
 
@@ -52,11 +50,15 @@ class GalleryViewerPage extends HookConsumerWidget {
     final isLoadOriginal = useState(AppSettingsEnum.loadOriginal.defaultValue);
     final isZoomed = useState<bool>(false);
     final showAppBar = useState<bool>(true);
-    final indexOfAsset = useState(assetList.indexOf(asset));
     final isPlayingMotionVideo = useState(false);
     final isPlayingVideo = useState(false);
     late Offset localPosition;
     final authToken = 'Bearer ${Store.get(StoreKey.accessToken)}';
+    final currentIndex = useState(initialIndex);
+    final currentAsset = loadAsset(currentIndex.value);
+    final watchedAsset = ref.watch(assetDetailProvider(currentAsset));
+
+    Asset asset() => watchedAsset.value ?? currentAsset;
 
     showAppBar.addListener(() {
       // Change to and from immersive mode, hiding navigation and app bar
@@ -79,16 +81,9 @@ class GalleryViewerPage extends HookConsumerWidget {
       [],
     );
 
-    void toggleFavorite(Asset asset) {
-      ref.watch(favoriteProvider.notifier).toggleFavorite(asset);
-    }
-
-    void getAssetExif() async {
-      assetDetail = assetList[indexOfAsset.value];
-      assetDetail = await ref
-          .watch(assetServiceProvider)
-          .loadExif(assetList[indexOfAsset.value]);
-    }
+    void toggleFavorite(Asset asset) => ref
+        .watch(assetProvider.notifier)
+        .toggleFavorite([asset], !asset.isFavorite);
 
     /// Thumbnail image of a remote asset. Required asset.isRemote
     ImageProvider remoteThumbnailImageProvider(
@@ -138,8 +133,8 @@ class GalleryViewerPage extends HookConsumerWidget {
     }
 
     void precacheNextImage(int index) {
-      if (index < assetList.length && index >= 0) {
-        final asset = assetList[index];
+      if (index < totalAssets && index >= 0) {
+        final asset = loadAsset(index);
 
         if (asset.isLocal) {
           // Preload the local asset
@@ -193,13 +188,13 @@ class GalleryViewerPage extends HookConsumerWidget {
           if (ref
               .watch(appSettingsServiceProvider)
               .getSetting<bool>(AppSettingsEnum.advancedTroubleshooting)) {
-            return AdvancedBottomSheet(assetDetail: assetDetail!);
+            return AdvancedBottomSheet(assetDetail: asset());
           }
           return Padding(
             padding: EdgeInsets.only(
               bottom: MediaQuery.of(context).viewInsets.bottom,
             ),
-            child: ExifBottomSheet(asset: assetDetail!),
+            child: ExifBottomSheet(asset: asset()),
           );
         },
       );
@@ -211,7 +206,7 @@ class GalleryViewerPage extends HookConsumerWidget {
         builder: (BuildContext _) {
           return DeleteDialog(
             onDelete: () {
-              if (assetList.length == 1) {
+              if (totalAssets == 1) {
                 // Handle only one asset
                 AutoRouter.of(context).pop();
               } else {
@@ -221,7 +216,6 @@ class GalleryViewerPage extends HookConsumerWidget {
                   curve: Curves.fastLinearToSlowEaseIn,
                 );
               }
-              assetList.remove(deleteAsset);
               ref.watch(assetProvider.notifier).deleteAssets({deleteAsset});
             },
           );
@@ -267,9 +261,7 @@ class GalleryViewerPage extends HookConsumerWidget {
     }
 
     shareAsset() {
-      ref
-          .watch(imageViewerStateProvider.notifier)
-          .shareAsset(assetList[indexOfAsset.value], context);
+      ref.watch(imageViewerStateProvider.notifier).shareAsset(asset(), context);
     }
 
     handleArchive(Asset asset) {
@@ -291,30 +283,21 @@ class GalleryViewerPage extends HookConsumerWidget {
           color: Colors.black.withOpacity(0.4),
           child: TopControlAppBar(
             isPlayingMotionVideo: isPlayingMotionVideo.value,
-            asset: assetList[indexOfAsset.value],
-            isFavorite: ref.watch(favoriteProvider).contains(
-                  assetList[indexOfAsset.value].id,
-                ),
-            onMoreInfoPressed: () {
-              showInfo();
-            },
-            onFavorite: () {
-              toggleFavorite(assetList[indexOfAsset.value]);
-            },
-            onDownloadPressed: assetList[indexOfAsset.value].storage ==
-                    AssetState.local
+            asset: asset(),
+            isFavorite: asset().isFavorite,
+            onMoreInfoPressed: showInfo,
+            onFavorite: asset().isRemote ? () => toggleFavorite(asset()) : null,
+            onDownloadPressed: asset().storage == AssetState.local
                 ? null
-                : () {
+                : () =>
                     ref.watch(imageViewerStateProvider.notifier).downloadAsset(
-                          assetList[indexOfAsset.value],
+                          asset(),
                           context,
-                        );
-                  },
+                        ),
             onToggleMotionVideo: (() {
               isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
             }),
-            onAddToAlbumPressed: () =>
-                addToAlbum(assetList[indexOfAsset.value]),
+            onAddToAlbumPressed: () => addToAlbum(asset()),
           ),
         ),
       );
@@ -324,8 +307,6 @@ class GalleryViewerPage extends HookConsumerWidget {
       final show = (showAppBar.value || // onTap has the final say
               (showAppBar.value && !isZoomed.value)) &&
           !isPlayingVideo.value;
-      final currentAsset = assetList[indexOfAsset.value];
-
       return AnimatedOpacity(
         duration: const Duration(milliseconds: 100),
         opacity: show ? 1.0 : 0.0,
@@ -343,7 +324,7 @@ class GalleryViewerPage extends HookConsumerWidget {
               label: 'control_bottom_app_bar_share'.tr(),
               tooltip: 'control_bottom_app_bar_share'.tr(),
             ),
-            currentAsset.isArchived
+            asset().isArchived
                 ? BottomNavigationBarItem(
                     icon: const Icon(Icons.unarchive_rounded),
                     label: 'control_bottom_app_bar_unarchive'.tr(),
@@ -366,10 +347,10 @@ class GalleryViewerPage extends HookConsumerWidget {
                 shareAsset();
                 break;
               case 1:
-                handleArchive(assetList[indexOfAsset.value]);
+                handleArchive(asset());
                 break;
               case 2:
-                handleDelete(assetList[indexOfAsset.value]);
+                handleDelete(asset());
                 break;
             }
           },
@@ -399,33 +380,33 @@ class GalleryViewerPage extends HookConsumerWidget {
                       ? const ScrollPhysics() // Use bouncing physics for iOS
                       : const ClampingScrollPhysics() // Use heavy physics for Android
                   ),
-              itemCount: assetList.length,
+              itemCount: totalAssets,
               scrollDirection: Axis.horizontal,
               onPageChanged: (value) {
                 // Precache image
-                if (indexOfAsset.value < value) {
+                if (currentIndex.value < value) {
                   // Moving forwards, so precache the next asset
                   precacheNextImage(value + 1);
                 } else {
                   // Moving backwards, so precache previous asset
                   precacheNextImage(value - 1);
                 }
-                indexOfAsset.value = value;
+                currentIndex.value = value;
                 HapticFeedback.selectionClick();
               },
               loadingBuilder: isLoadPreview.value
                   ? (context, event) {
-                      final asset = assetList[indexOfAsset.value];
-                      if (!asset.isLocal) {
+                      final a = asset();
+                      if (!a.isLocal) {
                         // Use the WEBP Thumbnail as a placeholder for the JPEG thumbnail to achieve
                         // Three-Stage Loading (WEBP -> JPEG -> Original)
                         final webPThumbnail = CachedNetworkImage(
                           imageUrl: getThumbnailUrl(
-                            asset,
+                            a,
                             type: api.ThumbnailFormat.WEBP,
                           ),
                           cacheKey: getThumbnailCacheKey(
-                            asset,
+                            a,
                             type: api.ThumbnailFormat.WEBP,
                           ),
                           httpHeaders: {'Authorization': authToken},
@@ -444,11 +425,11 @@ class GalleryViewerPage extends HookConsumerWidget {
                           // makes sense if the original is loaded in the builder
                           return CachedNetworkImage(
                             imageUrl: getThumbnailUrl(
-                              asset,
+                              a,
                               type: api.ThumbnailFormat.JPEG,
                             ),
                             cacheKey: getThumbnailCacheKey(
-                              asset,
+                              a,
                               type: api.ThumbnailFormat.JPEG,
                             ),
                             httpHeaders: {'Authorization': authToken},
@@ -462,30 +443,30 @@ class GalleryViewerPage extends HookConsumerWidget {
                         }
                       } else {
                         return Image(
-                          image: localThumbnailImageProvider(asset),
+                          image: localThumbnailImageProvider(a),
                           fit: BoxFit.contain,
                         );
                       }
                     }
                   : null,
               builder: (context, index) {
-                getAssetExif();
-                if (assetList[index].isImage && !isPlayingMotionVideo.value) {
+                final asset = loadAsset(index);
+                if (asset.isImage && !isPlayingMotionVideo.value) {
                   // Show photo
                   final ImageProvider provider;
-                  if (assetList[index].isLocal) {
-                    provider = localImageProvider(assetList[index]);
+                  if (asset.isLocal) {
+                    provider = localImageProvider(asset);
                   } else {
                     if (isLoadOriginal.value) {
-                      provider = originalImageProvider(assetList[index]);
+                      provider = originalImageProvider(asset);
                     } else if (isLoadPreview.value) {
                       provider = remoteThumbnailImageProvider(
-                        assetList[index],
+                        asset,
                         api.ThumbnailFormat.JPEG,
                       );
                     } else {
                       provider = remoteThumbnailImageProvider(
-                        assetList[index],
+                        asset,
                         api.ThumbnailFormat.WEBP,
                       );
                     }
@@ -499,13 +480,13 @@ class GalleryViewerPage extends HookConsumerWidget {
                         showAppBar.value = !showAppBar.value,
                     imageProvider: provider,
                     heroAttributes: PhotoViewHeroAttributes(
-                      tag: assetList[index].id,
+                      tag: asset.id,
                     ),
                     filterQuality: FilterQuality.high,
                     tightMode: true,
                     minScale: PhotoViewComputedScale.contained,
                     errorBuilder: (context, error, stackTrace) => ImmichImage(
-                      assetList[indexOfAsset.value],
+                      asset,
                       fit: BoxFit.contain,
                     ),
                   );
@@ -516,7 +497,7 @@ class GalleryViewerPage extends HookConsumerWidget {
                     onDragUpdate: (_, details, __) =>
                         handleSwipeUpDown(details),
                     heroAttributes: PhotoViewHeroAttributes(
-                      tag: assetList[index].id,
+                      tag: asset.id,
                     ),
                     filterQuality: FilterQuality.high,
                     maxScale: 1.0,
@@ -526,7 +507,7 @@ class GalleryViewerPage extends HookConsumerWidget {
                       child: VideoViewerPage(
                         onPlaying: () => isPlayingVideo.value = true,
                         onPaused: () => isPlayingVideo.value = false,
-                        asset: assetList[index],
+                        asset: asset,
                         isMotionVideo: isPlayingMotionVideo.value,
                         onVideoEnded: () {
                           if (isPlayingMotionVideo.value) {
diff --git a/mobile/lib/modules/favorite/providers/favorite_provider.dart b/mobile/lib/modules/favorite/providers/favorite_provider.dart
index 19fe396ce5..d95742f890 100644
--- a/mobile/lib/modules/favorite/providers/favorite_provider.dart
+++ b/mobile/lib/modules/favorite/providers/favorite_provider.dart
@@ -1,68 +1,25 @@
 import 'package:hooks_riverpod/hooks_riverpod.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/providers/asset.provider.dart';
+import 'package:immich_mobile/shared/models/store.dart';
+import 'package:immich_mobile/shared/providers/db.provider.dart';
+import 'package:isar/isar.dart';
 
-class FavoriteSelectionNotifier extends StateNotifier<Set<int>> {
-  FavoriteSelectionNotifier(this.assetsState, this.assetNotifier) : super({}) {
-    state = assetsState.allAssets
-        .where((asset) => asset.isFavorite)
-        .map((asset) => asset.id)
-        .toSet();
+final favoriteAssetsProvider = StreamProvider<RenderList>((ref) async* {
+  final query = ref
+      .watch(dbProvider)
+      .assets
+      .filter()
+      .ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
+      .isFavoriteEqualTo(true)
+      .sortByFileCreatedAt();
+  final settings = ref.watch(appSettingsServiceProvider);
+  final groupBy =
+      GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)];
+  yield await RenderList.fromQuery(query, groupBy);
+  await for (final _ in query.watchLazy()) {
+    yield await RenderList.fromQuery(query, groupBy);
   }
-
-  final AssetsState assetsState;
-  final AssetNotifier assetNotifier;
-
-  void _setFavoriteForAssetId(int id, bool favorite) {
-    if (!favorite) {
-      state = state.difference({id});
-    } else {
-      state = state.union({id});
-    }
-  }
-
-  bool _isFavorite(int id) {
-    return state.contains(id);
-  }
-
-  Future<void> toggleFavorite(Asset asset) async {
-    // TODO support local favorite assets
-    if (asset.storage == AssetState.local) return;
-    _setFavoriteForAssetId(asset.id, !_isFavorite(asset.id));
-
-    await assetNotifier.toggleFavorite(
-      asset,
-      state.contains(asset.id),
-    );
-  }
-
-  Future<void> addToFavorites(Iterable<Asset> assets) {
-    state = state.union(assets.map((a) => a.id).toSet());
-    final futures = assets.map(
-      (a) => assetNotifier.toggleFavorite(
-        a,
-        true,
-      ),
-    );
-
-    return Future.wait(futures);
-  }
-}
-
-final favoriteProvider =
-    StateNotifierProvider<FavoriteSelectionNotifier, Set<int>>((ref) {
-  return FavoriteSelectionNotifier(
-    ref.watch(assetProvider),
-    ref.watch(assetProvider.notifier),
-  );
-});
-
-final favoriteAssetProvider = StateProvider((ref) {
-  final favorites = ref.watch(favoriteProvider);
-
-  return ref
-      .watch(assetProvider)
-      .allAssets
-      .where((element) => favorites.contains(element.id))
-      .toList();
 });
diff --git a/mobile/lib/modules/favorite/ui/favorite_image.dart b/mobile/lib/modules/favorite/ui/favorite_image.dart
deleted file mode 100644
index 26bef32f96..0000000000
--- a/mobile/lib/modules/favorite/ui/favorite_image.dart
+++ /dev/null
@@ -1,36 +0,0 @@
-
-import 'package:auto_route/auto_route.dart';
-import 'package:flutter/material.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/routing/router.dart';
-import 'package:immich_mobile/shared/models/asset.dart';
-import 'package:immich_mobile/shared/ui/immich_image.dart';
-
-class FavoriteImage extends HookConsumerWidget {
-  final Asset asset;
-  final List<Asset> assets;
-
-  const FavoriteImage(this.asset, this.assets, {super.key});
-
-  @override
-  Widget build(BuildContext context, WidgetRef ref) {
-    void viewAsset() {
-      AutoRouter.of(context).push(
-        GalleryViewerRoute(
-          asset: asset,
-          assetList: assets,
-        ),
-      );
-    }
-
-    return GestureDetector(
-      onTap: viewAsset,
-      child: ImmichImage(
-        asset,
-        width: 300,
-        height: 300,
-      ),
-    );
-  }
-
-}
\ No newline at end of file
diff --git a/mobile/lib/modules/favorite/views/favorites_page.dart b/mobile/lib/modules/favorite/views/favorites_page.dart
index ff7e6ae62a..fa68ff03c2 100644
--- a/mobile/lib/modules/favorite/views/favorites_page.dart
+++ b/mobile/lib/modules/favorite/views/favorites_page.dart
@@ -1,15 +1,32 @@
 import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:fluttertoast/fluttertoast.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
+import 'package:immich_mobile/shared/models/asset.dart';
+import 'package:immich_mobile/shared/providers/asset.provider.dart';
+import 'package:immich_mobile/shared/ui/immich_toast.dart';
 
 class FavoritesPage extends HookConsumerWidget {
   const FavoritesPage({Key? key}) : super(key: key);
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
+    final selectionEnabledHook = useState(false);
+    final selection = useState(<Asset>{});
+    final processing = useState(false);
+
+    void selectionListener(
+      bool multiselect,
+      Set<Asset> selectedAssets,
+    ) {
+      selectionEnabledHook.value = multiselect;
+      selection.value = selectedAssets;
+    }
+
     AppBar buildAppBar() {
       return AppBar(
         leading: IconButton(
@@ -24,15 +41,77 @@ class FavoritesPage extends HookConsumerWidget {
       );
     }
 
+    void unfavorite() async {
+      try {
+        if (selection.value.isNotEmpty) {
+          await ref.watch(assetProvider.notifier).toggleFavorite(
+                selection.value.toList(),
+                false,
+              );
+          final assetOrAssets = selection.value.length > 1 ? 'assets' : 'asset';
+          ImmichToast.show(
+            context: context,
+            msg:
+                'Removed ${selection.value.length} $assetOrAssets from favorites',
+            gravity: ToastGravity.CENTER,
+          );
+        }
+      } finally {
+        processing.value = false;
+        selectionEnabledHook.value = false;
+      }
+    }
+
+    Widget buildBottomBar() {
+      return SafeArea(
+        child: Align(
+          alignment: Alignment.bottomCenter,
+          child: SizedBox(
+            height: 64,
+            child: Card(
+              child: Column(
+                children: [
+                  ListTile(
+                    shape: RoundedRectangleBorder(
+                      borderRadius: BorderRadius.circular(10),
+                    ),
+                    leading: const Icon(
+                      Icons.star_border,
+                    ),
+                    title: const Text(
+                      "Unfavorite",
+                      style: TextStyle(fontSize: 14),
+                    ),
+                    onTap: processing.value ? null : unfavorite,
+                  )
+                ],
+              ),
+            ),
+          ),
+        ),
+      );
+    }
+
     return Scaffold(
       appBar: buildAppBar(),
-      body: ref.watch(favoriteAssetProvider).isEmpty
-          ? Center(
-              child: Text('favorites_page_no_favorites'.tr()),
-            )
-          : ImmichAssetGrid(
-              assets: ref.watch(favoriteAssetProvider),
-            ),
+      body: ref.watch(favoriteAssetsProvider).when(
+            loading: () => const Center(child: CircularProgressIndicator()),
+            error: (error, stackTrace) => Center(child: Text(error.toString())),
+            data: (data) => data.isEmpty
+                ? Center(
+                    child: Text('favorites_page_no_favorites'.tr()),
+                  )
+                : Stack(
+                    children: [
+                      ImmichAssetGrid(
+                        renderList: data,
+                        selectionActive: selectionEnabledHook.value,
+                        listener: selectionListener,
+                      ),
+                      if (selectionEnabledHook.value) buildBottomBar()
+                    ],
+                  ),
+          ),
     );
   }
 }
diff --git a/mobile/lib/modules/home/ui/asset_grid/asset_grid_data_structure.dart b/mobile/lib/modules/home/ui/asset_grid/asset_grid_data_structure.dart
index b787ab783c..d5a0c0d928 100644
--- a/mobile/lib/modules/home/ui/asset_grid/asset_grid_data_structure.dart
+++ b/mobile/lib/modules/home/ui/asset_grid/asset_grid_data_structure.dart
@@ -2,212 +2,313 @@ import 'dart:math';
 
 import 'package:collection/collection.dart';
 import 'package:easy_localization/easy_localization.dart';
-import 'package:flutter/foundation.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
+import 'package:isar/isar.dart';
 import 'package:logging/logging.dart';
 
 final log = Logger('AssetGridDataStructure');
 
 enum RenderAssetGridElementType {
+  assets,
   assetRow,
   groupDividerTitle,
   monthTitle;
 }
 
-class RenderAssetGridRow {
-  final List<Asset> assets;
-  final List<double> widthDistribution;
-
-  RenderAssetGridRow(this.assets, this.widthDistribution);
-}
-
 class RenderAssetGridElement {
   final RenderAssetGridElementType type;
-  final RenderAssetGridRow? assetRow;
   final String? title;
   final DateTime date;
-  final List<Asset>? relatedAssetList;
+  final int count;
+  final int offset;
+  final int totalCount;
 
   RenderAssetGridElement(
     this.type, {
-    this.assetRow,
     this.title,
     required this.date,
-    this.relatedAssetList,
+    this.count = 0,
+    this.offset = 0,
+    this.totalCount = 0,
   });
 }
 
 enum GroupAssetsBy {
   day,
-  month;
-}
-
-class AssetGridLayoutParameters {
-  final int perRow;
-  final bool dynamicLayout;
-  final GroupAssetsBy groupBy;
-
-  AssetGridLayoutParameters(
-    this.perRow,
-    this.dynamicLayout,
-    this.groupBy,
-  );
-}
-
-class _AssetGroupsToRenderListComputeParameters {
-  final List<Asset> assets;
-  final AssetGridLayoutParameters layout;
-
-  _AssetGroupsToRenderListComputeParameters(
-    this.assets,
-    this.layout,
-  );
+  month,
+  auto,
+  none,
+  ;
 }
 
 class RenderList {
   final List<RenderAssetGridElement> elements;
+  final List<Asset>? allAssets;
+  final QueryBuilder<Asset, Asset, QAfterSortBy>? query;
+  final int totalAssets;
 
-  RenderList(this.elements);
+  /// reference to batch of assets loaded from DB with offset [_bufOffset]
+  List<Asset> _buf = [];
 
-  static Map<DateTime, List<Asset>> _groupAssets(
-    List<Asset> assets,
-    GroupAssetsBy groupBy,
-  ) {
-    if (groupBy == GroupAssetsBy.day) {
-      return assets.groupListsBy(
-        (element) {
-          final date = element.fileCreatedAt.toLocal();
-          return DateTime(date.year, date.month, date.day);
-        },
-      );
-    } else if (groupBy == GroupAssetsBy.month) {
-      return assets.groupListsBy(
-        (element) {
-          final date = element.fileCreatedAt.toLocal();
-          return DateTime(date.year, date.month);
-        },
-      );
+  /// global offset of assets in [_buf]
+  int _bufOffset = 0;
+
+  RenderList(this.elements, this.query, this.allAssets)
+      : totalAssets = allAssets?.length ?? query!.countSync();
+
+  bool get isEmpty => totalAssets == 0;
+
+  /// Loads the requested assets from the database to an internal buffer if not cached
+  /// and returns a slice of that buffer
+  List<Asset> loadAssets(int offset, int count) {
+    assert(offset >= 0);
+    assert(count > 0);
+    assert(offset + count <= totalAssets);
+    if (allAssets != null) {
+      // if we already loaded all assets (e.g. from search result)
+      // simply return the requested slice of that array
+      return allAssets!.slice(offset, offset + count);
+    } else if (query != null) {
+      // general case: we have the query to load assets via offset from the DB on demand
+      if (offset < _bufOffset || offset + count > _bufOffset + _buf.length) {
+        // the requested slice (offset:offset+count) is not contained in the cache buffer `_buf`
+        // thus, fill the buffer with a new batch of assets that at least contains the requested
+        // assets and some more
+
+        final bool forward = _bufOffset < offset;
+        // if the requested offset is greater than the cached offset, the user scrolls forward "down"
+        const batchSize = 256;
+        const oppositeSize = 64;
+
+        // make sure to load a meaningful amount of data (and not only the requested slice)
+        // otherwise, each call to [loadAssets] would result in DB call trashing performance
+        // fills small requests to [batchSize], adds some legroom into the opposite scroll direction for large requests
+        final len = max(batchSize, count + oppositeSize);
+        // when scrolling forward, start shortly before the requested offset...
+        // when scrolling backward, end shortly after the requested offset...
+        // ... to guard against the user scrolling in the other direction
+        // a tiny bit resulting in a another required load from the DB
+        final start = max(
+          0,
+          forward
+              ? offset - oppositeSize
+              : (len > batchSize ? offset : offset + count - len),
+        );
+        // load the calculated batch (start:start+len) from the DB and put it into the buffer
+        _buf = query!.offset(start).limit(len).findAllSync();
+        _bufOffset = start;
+      }
+      assert(_bufOffset <= offset);
+      assert(_bufOffset + _buf.length >= offset + count);
+      // return the requested slice from the buffer (we made sure before that the assets are loaded!)
+      return _buf.slice(offset - _bufOffset, offset - _bufOffset + count);
     }
-
-    return {};
+    throw Exception("RenderList has neither assets nor query");
   }
 
-  static Future<RenderList> _processAssetGroupData(
-    _AssetGroupsToRenderListComputeParameters data,
+  /// Returns the requested asset either from cached buffer or directly from the database
+  Asset loadAsset(int index) {
+    if (allAssets != null) {
+      // all assets are already loaded (e.g. from search result)
+      return allAssets![index];
+    } else if (query != null) {
+      // general case: we have the DB query to load asset(s) on demand
+      if (index >= _bufOffset && index < _bufOffset + _buf.length) {
+        // lucky case: the requested asset is already cached in the buffer!
+        return _buf[index - _bufOffset];
+      }
+      // request the asset from the database (not changing the buffer!)
+      final asset = query!.offset(index).findFirstSync();
+      if (asset == null) {
+        throw Exception(
+          "Asset at index $index does no longer exist in database",
+        );
+      }
+      return asset;
+    }
+    throw Exception("RenderList has neither assets nor query");
+  }
+
+  static Future<RenderList> fromQuery(
+    QueryBuilder<Asset, Asset, QAfterSortBy> query,
+    GroupAssetsBy groupBy,
+  ) =>
+      _buildRenderList(null, query, groupBy);
+
+  static Future<RenderList> _buildRenderList(
+    List<Asset>? assets,
+    QueryBuilder<Asset, Asset, QAfterSortBy>? query,
+    GroupAssetsBy groupBy,
   ) async {
-    // TODO: Make DateFormat use the configured locale.
-    final monthFormat = DateFormat.yMMM();
-    final dayFormatSameYear = DateFormat.MMMEd();
-    final dayFormatOtherYear = DateFormat.yMMMEd();
-    final allAssets = data.assets;
-    final perRow = data.layout.perRow;
-    final dynamicLayout = data.layout.dynamicLayout;
-    final groupBy = data.layout.groupBy;
+    final List<RenderAssetGridElement> elements = [];
 
-    List<RenderAssetGridElement> elements = [];
-    DateTime? lastDate;
-
-    final groups = _groupAssets(allAssets, groupBy);
-
-    groups.entries.sortedBy((e) => e.key).reversed.forEach((entry) {
-      final date = entry.key;
-      final assets = entry.value;
-
-      try {
-        // Month title
-        if (groupBy == GroupAssetsBy.day &&
-            (lastDate == null || lastDate!.month != date.month)) {
-          elements.add(
-            RenderAssetGridElement(
-              RenderAssetGridElementType.monthTitle,
-              title: monthFormat.format(date),
-              date: date,
-            ),
-          );
-        }
-
-        // Group divider title (day or month)
-        var formatDate = dayFormatOtherYear;
-
-        if (DateTime.now().year == date.year) {
-          formatDate = dayFormatSameYear;
-        }
-
-        if (groupBy == GroupAssetsBy.month) {
-          formatDate = monthFormat;
-        }
+    const pageSize = 500;
+    const sectionSize = 60; // divides evenly by 2,3,4,5,6
 
+    if (groupBy == GroupAssetsBy.none) {
+      final int total = assets?.length ?? query!.countSync();
+      for (int i = 0; i < total; i += sectionSize) {
+        final date = assets != null
+            ? assets[i].fileCreatedAt
+            : await query!.offset(i).fileCreatedAtProperty().findFirst();
+        final int count = i + sectionSize > total ? total - i : sectionSize;
+        if (date == null) break;
         elements.add(
           RenderAssetGridElement(
-            RenderAssetGridElementType.groupDividerTitle,
-            title: formatDate.format(date),
+            RenderAssetGridElementType.assets,
             date: date,
-            relatedAssetList: assets,
+            count: count,
+            totalCount: total,
+            offset: i,
           ),
         );
-
-        // Add rows
-        int cursor = 0;
-        while (cursor < assets.length) {
-          int rowElements = min(assets.length - cursor, perRow);
-          final rowAssets = assets.sublist(cursor, cursor + rowElements);
-
-          // Default: All assets have the same width
-          var widthDistribution = List.filled(rowElements, 1.0);
-
-          if (dynamicLayout) {
-            final aspectRatios =
-                rowAssets.map((e) => (e.width ?? 1) / (e.height ?? 1)).toList();
-            final meanAspectRatio = aspectRatios.sum / rowElements;
-
-            // 1: mean width
-            // 0.5: width < mean - threshold
-            // 1.5: width > mean + threshold
-            final arConfiguration = aspectRatios.map((e) {
-              if (e - meanAspectRatio > 0.3) return 1.5;
-              if (e - meanAspectRatio < -0.3) return 0.5;
-              return 1.0;
-            });
-
-            // Normalize:
-            final sum = arConfiguration.sum;
-            widthDistribution =
-                arConfiguration.map((e) => (e * rowElements) / sum).toList();
-          }
-
-          final rowElement = RenderAssetGridElement(
-            RenderAssetGridElementType.assetRow,
-            date: date,
-            assetRow: RenderAssetGridRow(
-              rowAssets,
-              widthDistribution,
-            ),
-          );
-
-          elements.add(rowElement);
-          cursor += rowElements;
-        }
-
-        lastDate = date;
-      } catch (e, stackTrace) {
-        log.severe(e, stackTrace);
       }
-    });
+      return RenderList(elements, query, assets);
+    }
 
-    return RenderList(elements);
+    final formatSameYear =
+        groupBy == GroupAssetsBy.month ? DateFormat.MMMM() : DateFormat.MMMEd();
+    final formatOtherYear = groupBy == GroupAssetsBy.month
+        ? DateFormat.yMMMM()
+        : DateFormat.yMMMEd();
+    final currentYear = DateTime.now().year;
+    final formatMergedSameYear = DateFormat.MMMd();
+    final formatMergedOtherYear = DateFormat.yMMMd();
+
+    int offset = 0;
+    DateTime? last;
+    DateTime? current;
+    int lastOffset = 0;
+    int count = 0;
+    int monthCount = 0;
+    int lastMonthIndex = 0;
+
+    String formatDateRange(DateTime from, DateTime to) {
+      final startDate = (from.year == currentYear
+              ? formatMergedSameYear
+              : formatMergedOtherYear)
+          .format(from);
+      final endDate = (to.year == currentYear
+              ? formatMergedSameYear
+              : formatMergedOtherYear)
+          .format(to);
+      if (DateTime(from.year, from.month, from.day) ==
+          DateTime(to.year, to.month, to.day)) {
+        // format range with time when both dates are on the same day
+        final startTime = DateFormat.Hm().format(from);
+        final endTime = DateFormat.Hm().format(to);
+        return "$startDate $startTime - $endTime";
+      }
+      return "$startDate - $endDate";
+    }
+
+    void mergeMonth() {
+      if (last != null &&
+          groupBy == GroupAssetsBy.auto &&
+          monthCount <= 30 &&
+          elements.length > lastMonthIndex + 1) {
+        // merge all days into a single section
+        assert(elements[lastMonthIndex].date.month == last.month);
+        final e = elements[lastMonthIndex];
+
+        elements[lastMonthIndex] = RenderAssetGridElement(
+          RenderAssetGridElementType.monthTitle,
+          date: e.date,
+          count: monthCount,
+          totalCount: monthCount,
+          offset: e.offset,
+          title: formatDateRange(e.date, elements.last.date),
+        );
+        elements.removeRange(lastMonthIndex + 1, elements.length);
+      }
+    }
+
+    void addElems(DateTime d, DateTime? prevDate) {
+      final bool newMonth =
+          last == null || last.year != d.year || last.month != d.month;
+      if (newMonth) {
+        mergeMonth();
+        lastMonthIndex = elements.length;
+        monthCount = 0;
+      }
+      for (int j = 0; j < count; j += sectionSize) {
+        final type = j == 0
+            ? (groupBy != GroupAssetsBy.month && newMonth
+                ? RenderAssetGridElementType.monthTitle
+                : RenderAssetGridElementType.groupDividerTitle)
+            : (groupBy == GroupAssetsBy.auto
+                ? RenderAssetGridElementType.groupDividerTitle
+                : RenderAssetGridElementType.assets);
+        final sectionCount = j + sectionSize > count ? count - j : sectionSize;
+        assert(sectionCount > 0 && sectionCount <= sectionSize);
+        elements.add(
+          RenderAssetGridElement(
+            type,
+            date: d,
+            count: sectionCount,
+            totalCount: groupBy == GroupAssetsBy.auto ? sectionCount : count,
+            offset: lastOffset + j,
+            title: j == 0
+                ? (d.year == currentYear
+                    ? formatSameYear.format(d)
+                    : formatOtherYear.format(d))
+                : (groupBy == GroupAssetsBy.auto
+                    ? formatDateRange(d, prevDate ?? d)
+                    : null),
+          ),
+        );
+      }
+      monthCount += count;
+    }
+
+    DateTime? prevDate;
+    while (true) {
+      // this iterates all assets (only their createdAt property) in batches
+      // memory usage is okay, however runtime is linear with number of assets
+      // TODO replace with groupBy once Isar supports such queries
+      final dates = assets != null
+          ? assets.map((a) => a.fileCreatedAt)
+          : await query!
+              .offset(offset)
+              .limit(pageSize)
+              .fileCreatedAtProperty()
+              .findAll();
+      int i = 0;
+      for (final date in dates) {
+        final d = DateTime(
+          date.year,
+          date.month,
+          groupBy == GroupAssetsBy.month ? 1 : date.day,
+        );
+        current ??= d;
+        if (current != d) {
+          addElems(current, prevDate);
+          last = current;
+          current = d;
+          lastOffset = offset + i;
+          count = 0;
+        }
+        prevDate = date;
+        count++;
+        i++;
+      }
+
+      if (assets != null || dates.length != pageSize) break;
+      offset += pageSize;
+    }
+    if (count > 0 && current != null) {
+      addElems(current, prevDate);
+      mergeMonth();
+    }
+    assert(elements.every((e) => e.count <= sectionSize), "too large section");
+    return RenderList(elements, query, assets);
   }
 
+  static RenderList empty() => RenderList([], null, []);
+
   static Future<RenderList> fromAssets(
     List<Asset> assets,
-    AssetGridLayoutParameters layout,
-  ) async {
-    // Compute only allows for one parameter. Therefore we pass all parameters in a map
-    return compute(
-      _processAssetGroupData,
-      _AssetGroupsToRenderListComputeParameters(
-        assets,
-        layout,
-      ),
-    );
-  }
+    GroupAssetsBy groupBy,
+  ) =>
+      _buildRenderList(assets, null, groupBy);
 }
diff --git a/mobile/lib/modules/home/ui/asset_grid/draggable_scrollbar_custom.dart b/mobile/lib/modules/home/ui/asset_grid/draggable_scrollbar_custom.dart
index dbdc125d28..d28d825c97 100644
--- a/mobile/lib/modules/home/ui/asset_grid/draggable_scrollbar_custom.dart
+++ b/mobile/lib/modules/home/ui/asset_grid/draggable_scrollbar_custom.dart
@@ -396,8 +396,8 @@ class DraggableScrollbarState extends State<DraggableScrollbar>
           widget.scrollStateListener(true);
 
           dragHaltTimer = Timer(
-            const Duration(milliseconds: 200),
-                () {
+            const Duration(milliseconds: 500),
+            () {
               widget.scrollStateListener(false);
             },
           );
diff --git a/mobile/lib/modules/home/ui/asset_grid/group_divider_title.dart b/mobile/lib/modules/home/ui/asset_grid/group_divider_title.dart
index 6a92c9e21f..cedfa46e51 100644
--- a/mobile/lib/modules/home/ui/asset_grid/group_divider_title.dart
+++ b/mobile/lib/modules/home/ui/asset_grid/group_divider_title.dart
@@ -19,8 +19,6 @@ class GroupDividerTitle extends ConsumerWidget {
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
-
-
     void handleTitleIconClick() {
       if (selected) {
         onDeselect();
@@ -32,7 +30,7 @@ class GroupDividerTitle extends ConsumerWidget {
     return Padding(
       padding: const EdgeInsets.only(
         top: 29.0,
-        bottom: 29.0,
+        bottom: 10.0,
         left: 12.0,
         right: 12.0,
       ),
diff --git a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart
index 021418df25..45f5b2ad4e 100644
--- a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart
+++ b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart
@@ -8,6 +8,7 @@ import 'package:immich_mobile/modules/settings/providers/app_settings.provider.d
 import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
 import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
+import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
 
 class ImmichAssetGrid extends HookConsumerWidget {
   final int? assetsPerRow;
@@ -15,13 +16,19 @@ class ImmichAssetGrid extends HookConsumerWidget {
   final bool? showStorageIndicator;
   final ImmichAssetGridSelectionListener? listener;
   final bool selectionActive;
-  final List<Asset> assets;
+  final List<Asset>? assets;
   final RenderList? renderList;
   final Future<void> Function()? onRefresh;
+  final Set<Asset>? preselectedAssets;
+  final bool canDeselect;
+  final bool? dynamicLayout;
+  final bool showMultiSelectIndicator;
+  final void Function(ItemPosition start, ItemPosition end)?
+      visibleItemsListener;
 
   const ImmichAssetGrid({
     super.key,
-    required this.assets,
+    this.assets,
     this.onRefresh,
     this.renderList,
     this.assetsPerRow,
@@ -29,12 +36,16 @@ class ImmichAssetGrid extends HookConsumerWidget {
     this.listener,
     this.margin = 5.0,
     this.selectionActive = false,
+    this.preselectedAssets,
+    this.canDeselect = true,
+    this.dynamicLayout,
+    this.showMultiSelectIndicator = true,
+    this.visibleItemsListener,
   });
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
     var settings = ref.watch(appSettingsServiceProvider);
-    final renderListFuture = ref.watch(renderListProvider(assets));
 
     // Needs to suppress hero animations when navigating to this widget
     final enableHeroAnimations = useState(false);
@@ -64,34 +75,12 @@ class ImmichAssetGrid extends HookConsumerWidget {
       return true;
     }
 
-    if (renderList != null) {
+    Widget buildAssetGridView(RenderList renderList) {
       return WillPopScope(
         onWillPop: onWillPop,
         child: HeroMode(
           enabled: enableHeroAnimations.value,
           child: ImmichAssetGridView(
-            allAssets: assets,
-            onRefresh: onRefresh,
-            assetsPerRow: assetsPerRow ??
-                settings.getSetting(AppSettingsEnum.tilesPerRow),
-            listener: listener,
-            showStorageIndicator: showStorageIndicator ??
-                settings.getSetting(AppSettingsEnum.storageIndicator),
-            renderList: renderList!,
-            margin: margin,
-            selectionActive: selectionActive,
-          ),
-        ),
-      );
-    }
-
-    return renderListFuture.when(
-      data: (renderList) => WillPopScope(
-        onWillPop: onWillPop,
-        child: HeroMode(
-          enabled: enableHeroAnimations.value,
-          child: ImmichAssetGridView(
-            allAssets: assets,
             onRefresh: onRefresh,
             assetsPerRow: assetsPerRow ??
                 settings.getSetting(AppSettingsEnum.tilesPerRow),
@@ -101,9 +90,22 @@ class ImmichAssetGrid extends HookConsumerWidget {
             renderList: renderList,
             margin: margin,
             selectionActive: selectionActive,
+            preselectedAssets: preselectedAssets,
+            canDeselect: canDeselect,
+            dynamicLayout: dynamicLayout ??
+                settings.getSetting(AppSettingsEnum.dynamicLayout),
+            showMultiSelectIndicator: showMultiSelectIndicator,
+            visibleItemsListener: visibleItemsListener,
           ),
         ),
-      ),
+      );
+    }
+
+    if (renderList != null) return buildAssetGridView(renderList!);
+
+    final renderListFuture = ref.watch(renderListProvider(assets!));
+    return renderListFuture.when(
+      data: (renderList) => buildAssetGridView(renderList),
       error: (err, stack) => Center(child: Text("$err")),
       loading: () => const Center(
         child: ImmichLoadingIndicator(),
diff --git a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart
index 0b3b447c63..c564927ad9 100644
--- a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart
+++ b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid_view.dart
@@ -1,4 +1,5 @@
 import 'dart:collection';
+import 'dart:math';
 
 import 'package:collection/collection.dart';
 import 'package:easy_localization/easy_localization.dart';
@@ -6,6 +7,7 @@ import 'package:flutter/material.dart';
 import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
+import 'package:immich_mobile/utils/builtin_extensions.dart';
 import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
 import 'asset_grid_data_structure.dart';
 import 'group_divider_title.dart';
@@ -23,13 +25,11 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
       ItemPositionsListener.create();
 
   bool _scrolling = false;
-  final Set<int> _selectedAssets = HashSet();
+  final Set<Asset> _selectedAssets =
+      HashSet(equals: (a, b) => a.id == b.id, hashCode: (a) => a.id);
 
   Set<Asset> _getSelectedAssets() {
-    return _selectedAssets
-        .map((e) => widget.allAssets.firstWhereOrNull((a) => a.id == e))
-        .whereNotNull()
-        .toSet();
+    return Set.from(_selectedAssets);
   }
 
   void _callSelectionListener(bool selectionActive) {
@@ -38,18 +38,14 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
 
   void _selectAssets(List<Asset> assets) {
     setState(() {
-      for (var e in assets) {
-        _selectedAssets.add(e.id);
-      }
+      _selectedAssets.addAll(assets);
       _callSelectionListener(true);
     });
   }
 
   void _deselectAssets(List<Asset> assets) {
     setState(() {
-      for (var e in assets) {
-        _selectedAssets.remove(e.id);
-      }
+      _selectedAssets.removeAll(assets);
       _callSelectionListener(_selectedAssets.isNotEmpty);
     });
   }
@@ -57,64 +53,86 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
   void _deselectAll() {
     setState(() {
       _selectedAssets.clear();
+      if (!widget.canDeselect &&
+          widget.preselectedAssets != null &&
+          widget.preselectedAssets!.isNotEmpty) {
+        _selectedAssets.addAll(widget.preselectedAssets!);
+      }
+      _callSelectionListener(false);
     });
-
-    _callSelectionListener(false);
   }
 
   bool _allAssetsSelected(List<Asset> assets) {
     return widget.selectionActive &&
-        assets.firstWhereOrNull((e) => !_selectedAssets.contains(e.id)) == null;
+        assets.firstWhereOrNull((e) => !_selectedAssets.contains(e)) == null;
   }
 
-  Widget _buildThumbnailOrPlaceholder(
-    Asset asset,
-    bool placeholder,
-  ) {
-    if (placeholder) {
-      return const DecoratedBox(
-        decoration: BoxDecoration(color: Colors.grey),
-      );
-    }
+  Widget _buildThumbnailOrPlaceholder(Asset asset, int index) {
     return ThumbnailImage(
       asset: asset,
-      assetList: widget.allAssets,
+      index: index,
+      loadAsset: widget.renderList.loadAsset,
+      totalAssets: widget.renderList.totalAssets,
       multiselectEnabled: widget.selectionActive,
-      isSelected: widget.selectionActive && _selectedAssets.contains(asset.id),
+      isSelected: widget.selectionActive && _selectedAssets.contains(asset),
       onSelect: () => _selectAssets([asset]),
-      onDeselect: () => _deselectAssets([asset]),
+      onDeselect: widget.canDeselect ||
+              widget.preselectedAssets == null ||
+              !widget.preselectedAssets!.contains(asset)
+          ? () => _deselectAssets([asset])
+          : null,
       useGrayBoxPlaceholder: true,
       showStorageIndicator: widget.showStorageIndicator,
     );
   }
 
   Widget _buildAssetRow(
+    Key key,
     BuildContext context,
-    RenderAssetGridRow row,
-    bool scrolling,
+    List<Asset> assets,
+    int absoluteOffset,
+    double width,
   ) {
-    return LayoutBuilder(
-      builder: (context, constraints) {
-        final size = constraints.maxWidth / widget.assetsPerRow -
-            widget.margin * (widget.assetsPerRow - 1) / widget.assetsPerRow;
-        return Row(
-          key: Key("asset-row-${row.assets.first.id}"),
-          children: row.assets.mapIndexed((int index, Asset asset) {
-            bool last = asset.id == row.assets.last.id;
+    // Default: All assets have the same width
+    final widthDistribution = List.filled(assets.length, 1.0);
 
-            return Container(
-              key: Key("asset-${asset.id}"),
-              width: size * row.widthDistribution[index],
-              height: size,
-              margin: EdgeInsets.only(
-                top: widget.margin,
-                right: last ? 0.0 : widget.margin,
-              ),
-              child: _buildThumbnailOrPlaceholder(asset, scrolling),
-            );
-          }).toList(),
+    if (widget.dynamicLayout) {
+      final aspectRatios =
+          assets.map((e) => (e.width ?? 1) / (e.height ?? 1)).toList();
+      final meanAspectRatio = aspectRatios.sum / assets.length;
+
+      // 1: mean width
+      // 0.5: width < mean - threshold
+      // 1.5: width > mean + threshold
+      final arConfiguration = aspectRatios.map((e) {
+        if (e - meanAspectRatio > 0.3) return 1.5;
+        if (e - meanAspectRatio < -0.3) return 0.5;
+        return 1.0;
+      });
+
+      // Normalize:
+      final sum = arConfiguration.sum;
+      widthDistribution.setRange(
+        0,
+        widthDistribution.length,
+        arConfiguration.map((e) => (e * assets.length) / sum),
+      );
+    }
+    return Row(
+      key: key,
+      children: assets.mapIndexed((int index, Asset asset) {
+        final bool last = index + 1 == widget.assetsPerRow;
+        return Container(
+          key: ValueKey(index),
+          width: width * widthDistribution[index],
+          height: width,
+          margin: EdgeInsets.only(
+            top: widget.margin,
+            right: last ? 0.0 : widget.margin,
+          ),
+          child: _buildThumbnailOrPlaceholder(asset, absoluteOffset + index),
         );
-      },
+      }).toList(),
     );
   }
 
@@ -132,10 +150,14 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
     );
   }
 
-  Widget _buildMonthTitle(BuildContext context, String title) {
+  Widget _buildMonthTitle(BuildContext context, DateTime date) {
+    final monthFormat = DateTime.now().year == date.year
+        ? DateFormat.MMMM()
+        : DateFormat.yMMMM();
+    final String title = monthFormat.format(date);
     return Padding(
       key: Key("month-$title"),
-      padding: const EdgeInsets.only(left: 12.0, top: 32),
+      padding: const EdgeInsets.only(left: 12.0, top: 30),
       child: Text(
         title,
         style: TextStyle(
@@ -147,18 +169,84 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
     );
   }
 
+  Widget _buildPlaceHolderRow(Key key, int num, double width, double height) {
+    return Row(
+      key: key,
+      children: [
+        for (int i = 0; i < num; i++)
+          Container(
+            key: ValueKey(i),
+            width: width,
+            height: height,
+            margin: EdgeInsets.only(
+              top: widget.margin,
+              right: i + 1 == num ? 0.0 : widget.margin,
+            ),
+            color: Colors.grey,
+          )
+      ],
+    );
+  }
+
+  Widget _buildSection(
+    BuildContext context,
+    RenderAssetGridElement section,
+    bool scrolling,
+  ) {
+    return LayoutBuilder(
+      builder: (context, constraints) {
+        final width = constraints.maxWidth / widget.assetsPerRow -
+            widget.margin * (widget.assetsPerRow - 1) / widget.assetsPerRow;
+        final rows =
+            (section.count + widget.assetsPerRow - 1) ~/ widget.assetsPerRow;
+        final List<Asset> assetsToRender = scrolling
+            ? []
+            : widget.renderList.loadAssets(section.offset, section.count);
+        return Column(
+          key: ValueKey(section.offset),
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            if (section.type == RenderAssetGridElementType.monthTitle)
+              _buildMonthTitle(context, section.date),
+            if (section.type == RenderAssetGridElementType.groupDividerTitle ||
+                section.type == RenderAssetGridElementType.monthTitle)
+              _buildTitle(
+                context,
+                section.title!,
+                scrolling
+                    ? []
+                    : widget.renderList
+                        .loadAssets(section.offset, section.totalCount),
+              ),
+            for (int i = 0; i < rows; i++)
+              scrolling
+                  ? _buildPlaceHolderRow(
+                      ValueKey(i),
+                      i + 1 == rows
+                          ? section.count - i * widget.assetsPerRow
+                          : widget.assetsPerRow,
+                      width,
+                      width,
+                    )
+                  : _buildAssetRow(
+                      ValueKey(i),
+                      context,
+                      assetsToRender.nestedSlice(
+                        i * widget.assetsPerRow,
+                        min((i + 1) * widget.assetsPerRow, section.count),
+                      ),
+                      section.offset + i * widget.assetsPerRow,
+                      width,
+                    ),
+          ],
+        );
+      },
+    );
+  }
+
   Widget _itemBuilder(BuildContext c, int position) {
     final item = widget.renderList.elements[position];
-
-    if (item.type == RenderAssetGridElementType.groupDividerTitle) {
-      return _buildTitle(c, item.title!, item.relatedAssetList!);
-    } else if (item.type == RenderAssetGridElementType.monthTitle) {
-      return _buildMonthTitle(c, item.title!);
-    } else if (item.type == RenderAssetGridElementType.assetRow) {
-      return _buildAssetRow(c, item.assetRow!, _scrolling);
-    }
-
-    return const Text("Invalid widget type!");
+    return _buildSection(c, item, _scrolling);
   }
 
   Text _labelBuilder(int pos) {
@@ -180,7 +268,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
   }
 
   Widget _buildAssetGrid() {
-    final useDragScrolling = widget.allAssets.length >= 20;
+    final useDragScrolling = widget.renderList.totalAssets >= 20;
 
     void dragScrolling(bool active) {
       setState(() {
@@ -225,6 +313,10 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
       setState(() {
         _selectedAssets.clear();
       });
+    } else if (widget.preselectedAssets != null) {
+      setState(() {
+        _selectedAssets.addAll(widget.preselectedAssets!);
+      });
     }
   }
 
@@ -241,14 +333,33 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
   void initState() {
     super.initState();
     scrollToTopNotifierProvider.addListener(_scrollToTop);
+    if (widget.visibleItemsListener != null) {
+      _itemPositionsListener.itemPositions.addListener(_positionListener);
+    }
   }
 
   @override
   void dispose() {
     scrollToTopNotifierProvider.removeListener(_scrollToTop);
+    if (widget.visibleItemsListener != null) {
+      _itemPositionsListener.itemPositions.removeListener(_positionListener);
+    }
     super.dispose();
   }
 
+  void _positionListener() {
+    final values = _itemPositionsListener.itemPositions.value;
+    final start = values.firstOrNull;
+    final end = values.lastOrNull;
+    if (start != null && end != null) {
+      if (start.index <= end.index) {
+        widget.visibleItemsListener?.call(start, end);
+      } else {
+        widget.visibleItemsListener?.call(end, start);
+      }
+    }
+  }
+
   void _scrollToTop() {
     // for some reason, this is necessary as well in order
     // to correctly reposition the drag thumb scroll bar
@@ -268,7 +379,8 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
       child: Stack(
         children: [
           _buildAssetGrid(),
-          if (widget.selectionActive) _buildMultiSelectIndicator(),
+          if (widget.showMultiSelectIndicator && widget.selectionActive)
+            _buildMultiSelectIndicator(),
         ],
       ),
     );
@@ -282,19 +394,28 @@ class ImmichAssetGridView extends StatefulWidget {
   final bool showStorageIndicator;
   final ImmichAssetGridSelectionListener? listener;
   final bool selectionActive;
-  final List<Asset> allAssets;
   final Future<void> Function()? onRefresh;
+  final Set<Asset>? preselectedAssets;
+  final bool canDeselect;
+  final bool dynamicLayout;
+  final bool showMultiSelectIndicator;
+  final void Function(ItemPosition start, ItemPosition end)?
+      visibleItemsListener;
 
   const ImmichAssetGridView({
     super.key,
     required this.renderList,
-    required this.allAssets,
     required this.assetsPerRow,
     required this.showStorageIndicator,
     this.listener,
     this.margin = 5.0,
     this.selectionActive = false,
     this.onRefresh,
+    this.preselectedAssets,
+    this.canDeselect = true,
+    this.dynamicLayout = true,
+    this.showMultiSelectIndicator = true,
+    this.visibleItemsListener,
   });
 
   @override
diff --git a/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart b/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart
index ff01ee9131..1373df7d3c 100644
--- a/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart
+++ b/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart
@@ -2,7 +2,6 @@ import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
 import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
 import 'package:immich_mobile/shared/ui/immich_image.dart';
@@ -10,7 +9,9 @@ import 'package:immich_mobile/utils/storage_indicator.dart';
 
 class ThumbnailImage extends HookConsumerWidget {
   final Asset asset;
-  final List<Asset> assetList;
+  final int index;
+  final Asset Function(int index) loadAsset;
+  final int totalAssets;
   final bool showStorageIndicator;
   final bool useGrayBoxPlaceholder;
   final bool isSelected;
@@ -21,7 +22,9 @@ class ThumbnailImage extends HookConsumerWidget {
   const ThumbnailImage({
     Key? key,
     required this.asset,
-    required this.assetList,
+    required this.index,
+    required this.loadAsset,
+    required this.totalAssets,
     this.showStorageIndicator = true,
     this.useGrayBoxPlaceholder = false,
     this.isSelected = false,
@@ -57,8 +60,9 @@ class ThumbnailImage extends HookConsumerWidget {
         } else {
           AutoRouter.of(context).push(
             GalleryViewerRoute(
-              assetList: assetList,
-              asset: asset,
+              initialIndex: index,
+              loadAsset: loadAsset,
+              totalAssets: totalAssets,
             ),
           );
         }
@@ -100,7 +104,9 @@ class ThumbnailImage extends HookConsumerWidget {
               decoration: BoxDecoration(
                 border: multiselectEnabled && isSelected
                     ? Border.all(
-                        color: Theme.of(context).primaryColorLight,
+                        color: onDeselect == null
+                            ? Colors.grey
+                            : Theme.of(context).primaryColorLight,
                         width: 10,
                       )
                     : const Border(),
@@ -130,7 +136,7 @@ class ThumbnailImage extends HookConsumerWidget {
                   size: 18,
                 ),
               ),
-            if (ref.watch(favoriteProvider).contains(asset.id))
+            if (asset.isFavorite)
               const Positioned(
                 left: 10,
                 bottom: 5,
diff --git a/mobile/lib/modules/home/ui/control_bottom_app_bar.dart b/mobile/lib/modules/home/ui/control_bottom_app_bar.dart
index b852dbd44b..df5321a535 100644
--- a/mobile/lib/modules/home/ui/control_bottom_app_bar.dart
+++ b/mobile/lib/modules/home/ui/control_bottom_app_bar.dart
@@ -7,15 +7,16 @@ import 'package:immich_mobile/shared/ui/drag_sheet.dart';
 import 'package:immich_mobile/shared/models/album.dart';
 
 class ControlBottomAppBar extends ConsumerWidget {
-  final Function onShare;
-  final Function onFavorite;
-  final Function onArchive;
-  final Function onDelete;
+  final void Function() onShare;
+  final void Function() onFavorite;
+  final void Function() onArchive;
+  final void Function() onDelete;
   final Function(Album album) onAddToAlbum;
   final void Function() onCreateNewAlbum;
 
   final List<Album> albums;
   final List<Album> sharedAlbums;
+  final bool enabled;
 
   const ControlBottomAppBar({
     Key? key,
@@ -27,6 +28,7 @@ class ControlBottomAppBar extends ConsumerWidget {
     required this.albums,
     required this.onAddToAlbum,
     required this.onCreateNewAlbum,
+    this.enabled = true,
   }) : super(key: key);
 
   @override
@@ -39,35 +41,31 @@ class ControlBottomAppBar extends ConsumerWidget {
           ControlBoxButton(
             iconData: Icons.ios_share_rounded,
             label: "control_bottom_app_bar_share".tr(),
-            onPressed: () {
-              onShare();
-            },
+            onPressed: enabled ? onShare : null,
           ),
           ControlBoxButton(
             iconData: Icons.favorite_border_rounded,
             label: "control_bottom_app_bar_favorite".tr(),
-            onPressed: () {
-              onFavorite();
-            },
+            onPressed: enabled ? onFavorite : null,
           ),
           ControlBoxButton(
             iconData: Icons.delete_outline_rounded,
             label: "control_bottom_app_bar_delete".tr(),
-            onPressed: () {
-              showDialog(
-                context: context,
-                builder: (BuildContext context) {
-                  return DeleteDialog(
-                    onDelete: onDelete,
-                  );
-                },
-              );
-            },
+            onPressed: enabled
+                ? () => showDialog(
+                      context: context,
+                      builder: (BuildContext context) {
+                        return DeleteDialog(
+                          onDelete: onDelete,
+                        );
+                      },
+                    )
+                : null,
           ),
           ControlBoxButton(
             iconData: Icons.archive,
             label: "control_bottom_app_bar_archive".tr(),
-            onPressed: () => onArchive(),
+            onPressed: enabled ? onArchive : null,
           ),
         ],
       );
@@ -108,7 +106,9 @@ class ControlBottomAppBar extends ConsumerWidget {
                       endIndent: 16,
                       thickness: 1,
                     ),
-                    AddToAlbumTitleRow(onCreateNewAlbum: onCreateNewAlbum),
+                    AddToAlbumTitleRow(
+                      onCreateNewAlbum: enabled ? onCreateNewAlbum : null,
+                    ),
                   ],
                 ),
               ),
@@ -118,6 +118,7 @@ class ControlBottomAppBar extends ConsumerWidget {
                   albums: albums,
                   sharedAlbums: sharedAlbums,
                   onAddToAlbum: onAddToAlbum,
+                  enabled: enabled,
                 ),
               ),
               const SliverToBoxAdapter(
@@ -137,7 +138,7 @@ class AddToAlbumTitleRow extends StatelessWidget {
     required this.onCreateNewAlbum,
   });
 
-  final VoidCallback onCreateNewAlbum;
+  final VoidCallback? onCreateNewAlbum;
 
   @override
   Widget build(BuildContext context) {
diff --git a/mobile/lib/modules/home/views/home_page.dart b/mobile/lib/modules/home/views/home_page.dart
index f6c4ea2d4e..a7caf511f1 100644
--- a/mobile/lib/modules/home/views/home_page.dart
+++ b/mobile/lib/modules/home/views/home_page.dart
@@ -10,14 +10,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/modules/album/providers/album.provider.dart';
 import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
 import 'package:immich_mobile/modules/album/services/album.service.dart';
-import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
 import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
 import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart';
 import 'package:immich_mobile/modules/home/ui/home_page_app_bar.dart';
 import 'package:immich_mobile/modules/home/ui/profile_drawer/profile_drawer.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/routing/router.dart';
 import 'package:immich_mobile/shared/models/album.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
@@ -34,7 +31,6 @@ class HomePage extends HookConsumerWidget {
 
   @override
   Widget build(BuildContext context, WidgetRef ref) {
-    final appSettingService = ref.watch(appSettingsServiceProvider);
     final multiselectEnabled = ref.watch(multiselectProvider.notifier);
     final selectionEnabledHook = useState(false);
 
@@ -45,6 +41,7 @@ class HomePage extends HookConsumerWidget {
 
     final tipOneOpacity = useState(0.0);
     final refreshCount = useState(0);
+    final processing = useState(false);
 
     useEffect(
       () {
@@ -97,7 +94,7 @@ class HomePage extends HookConsumerWidget {
         selectionEnabledHook.value = false;
       }
 
-      Iterable<Asset> remoteOnlySelection({String? localErrorMessage}) {
+      List<Asset> remoteOnlySelection({String? localErrorMessage}) {
         final Set<Asset> assets = selection.value;
         final bool onlyRemote = assets.every((e) => e.isRemote);
         if (!onlyRemote) {
@@ -108,113 +105,139 @@ class HomePage extends HookConsumerWidget {
               gravity: ToastGravity.BOTTOM,
             );
           }
-          return assets.where((a) => a.isRemote);
+          return assets.where((a) => a.isRemote).toList();
         }
-        return assets;
+        return assets.toList();
       }
 
-      void onFavoriteAssets() {
-        final remoteAssets = remoteOnlySelection(
-          localErrorMessage: 'home_page_favorite_err_local'.tr(),
-        );
-        if (remoteAssets.isNotEmpty) {
-          ref.watch(favoriteProvider.notifier).addToFavorites(remoteAssets);
-
-          final assetOrAssets = remoteAssets.length > 1 ? 'assets' : 'asset';
-          ImmichToast.show(
-            context: context,
-            msg: 'Added ${remoteAssets.length} $assetOrAssets to favorites',
-            gravity: ToastGravity.BOTTOM,
+      void onFavoriteAssets() async {
+        processing.value = true;
+        try {
+          final remoteAssets = remoteOnlySelection(
+            localErrorMessage: 'home_page_favorite_err_local'.tr(),
           );
-        }
+          if (remoteAssets.isNotEmpty) {
+            await ref
+                .watch(assetProvider.notifier)
+                .toggleFavorite(remoteAssets, true);
 
-        selectionEnabledHook.value = false;
+            final assetOrAssets = remoteAssets.length > 1 ? 'assets' : 'asset';
+            ImmichToast.show(
+              context: context,
+              msg: 'Added ${remoteAssets.length} $assetOrAssets to favorites',
+              gravity: ToastGravity.BOTTOM,
+            );
+          }
+        } finally {
+          processing.value = false;
+          selectionEnabledHook.value = false;
+        }
       }
 
-      void onArchiveAsset() {
-        final remoteAssets = remoteOnlySelection(
-          localErrorMessage: 'home_page_archive_err_local'.tr(),
-        );
-        if (remoteAssets.isNotEmpty) {
-          ref.watch(assetProvider.notifier).toggleArchive(remoteAssets, true);
-
-          final assetOrAssets = remoteAssets.length > 1 ? 'assets' : 'asset';
-          ImmichToast.show(
-            context: context,
-            msg: 'Moved ${remoteAssets.length} $assetOrAssets to archive',
-            gravity: ToastGravity.CENTER,
+      void onArchiveAsset() async {
+        processing.value = true;
+        try {
+          final remoteAssets = remoteOnlySelection(
+            localErrorMessage: 'home_page_archive_err_local'.tr(),
           );
-        }
+          if (remoteAssets.isNotEmpty) {
+            await ref
+                .watch(assetProvider.notifier)
+                .toggleArchive(remoteAssets, true);
 
-        selectionEnabledHook.value = false;
+            final assetOrAssets = remoteAssets.length > 1 ? 'assets' : 'asset';
+            ImmichToast.show(
+              context: context,
+              msg: 'Moved ${remoteAssets.length} $assetOrAssets to archive',
+              gravity: ToastGravity.CENTER,
+            );
+          }
+        } finally {
+          processing.value = false;
+          selectionEnabledHook.value = false;
+        }
       }
 
-      void onDelete() {
-        ref.watch(assetProvider.notifier).deleteAssets(selection.value);
-        selectionEnabledHook.value = false;
+      void onDelete() async {
+        processing.value = true;
+        try {
+          await ref.watch(assetProvider.notifier).deleteAssets(selection.value);
+          selectionEnabledHook.value = false;
+        } finally {
+          processing.value = false;
+        }
       }
 
       void onAddToAlbum(Album album) async {
-        final Iterable<Asset> assets = remoteOnlySelection(
-          localErrorMessage: "home_page_add_to_album_err_local".tr(),
-        );
-        if (assets.isEmpty) {
-          return;
-        }
-        final result = await albumService.addAdditionalAssetToAlbum(
-          assets,
-          album,
-        );
-
-        if (result != null) {
-          if (result.alreadyInAlbum.isNotEmpty) {
-            ImmichToast.show(
-              context: context,
-              msg: "home_page_add_to_album_conflicts".tr(
-                namedArgs: {
-                  "album": album.name,
-                  "added": result.successfullyAdded.toString(),
-                  "failed": result.alreadyInAlbum.length.toString()
-                },
-              ),
-            );
-          } else {
-            ImmichToast.show(
-              context: context,
-              msg: "home_page_add_to_album_success".tr(
-                namedArgs: {
-                  "album": album.name,
-                  "added": result.successfullyAdded.toString(),
-                },
-              ),
-              toastType: ToastType.success,
-            );
+        processing.value = true;
+        try {
+          final Iterable<Asset> assets = remoteOnlySelection(
+            localErrorMessage: "home_page_add_to_album_err_local".tr(),
+          );
+          if (assets.isEmpty) {
+            return;
           }
+          final result = await albumService.addAdditionalAssetToAlbum(
+            assets,
+            album,
+          );
 
+          if (result != null) {
+            if (result.alreadyInAlbum.isNotEmpty) {
+              ImmichToast.show(
+                context: context,
+                msg: "home_page_add_to_album_conflicts".tr(
+                  namedArgs: {
+                    "album": album.name,
+                    "added": result.successfullyAdded.toString(),
+                    "failed": result.alreadyInAlbum.length.toString()
+                  },
+                ),
+              );
+            } else {
+              ImmichToast.show(
+                context: context,
+                msg: "home_page_add_to_album_success".tr(
+                  namedArgs: {
+                    "album": album.name,
+                    "added": result.successfullyAdded.toString(),
+                  },
+                ),
+                toastType: ToastType.success,
+              );
+            }
+          }
+        } finally {
+          processing.value = false;
           selectionEnabledHook.value = false;
         }
       }
 
       void onCreateNewAlbum() async {
-        final Iterable<Asset> assets = remoteOnlySelection(
-          localErrorMessage: "home_page_add_to_album_err_local".tr(),
-        );
-        if (assets.isEmpty) {
-          return;
-        }
-        final result = await albumService.createAlbumWithGeneratedName(assets);
+        processing.value = true;
+        try {
+          final Iterable<Asset> assets = remoteOnlySelection(
+            localErrorMessage: "home_page_add_to_album_err_local".tr(),
+          );
+          if (assets.isEmpty) {
+            return;
+          }
+          final result =
+              await albumService.createAlbumWithGeneratedName(assets);
 
-        if (result != null) {
-          ref.watch(albumProvider.notifier).getAllAlbums();
-          ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
-          selectionEnabledHook.value = false;
+          if (result != null) {
+            ref.watch(albumProvider.notifier).getAllAlbums();
+            ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
+            selectionEnabledHook.value = false;
 
-          AutoRouter.of(context).push(AlbumViewerRoute(albumId: result.id));
+            AutoRouter.of(context).push(AlbumViewerRoute(albumId: result.id));
+          }
+        } finally {
+          processing.value = false;
         }
       }
 
       Future<void> refreshAssets() async {
-        debugPrint("refreshCount.value ${refreshCount.value}");
         final fullRefresh = refreshCount.value > 0;
         await ref.read(assetProvider.notifier).getAllAsset(clear: fullRefresh);
         if (fullRefresh) {
@@ -277,20 +300,18 @@ class HomePage extends HookConsumerWidget {
         bottom: false,
         child: Stack(
           children: [
-            ref.watch(assetProvider).renderList == null ||
-                    ref.watch(assetProvider).allAssets.isEmpty
-                ? buildLoadingIndicator()
-                : ImmichAssetGrid(
-                    renderList: ref.watch(assetProvider).renderList!,
-                    assets: ref.read(assetProvider).allAssets,
-                    assetsPerRow: appSettingService
-                        .getSetting(AppSettingsEnum.tilesPerRow),
-                    showStorageIndicator: appSettingService
-                        .getSetting(AppSettingsEnum.storageIndicator),
-                    listener: selectionListener,
-                    selectionActive: selectionEnabledHook.value,
-                    onRefresh: refreshAssets,
-                  ),
+            ref.watch(assetsProvider).when(
+                  data: (data) => data.isEmpty
+                      ? buildLoadingIndicator()
+                      : ImmichAssetGrid(
+                          renderList: data,
+                          listener: selectionListener,
+                          selectionActive: selectionEnabledHook.value,
+                          onRefresh: refreshAssets,
+                        ),
+                  error: (error, _) => Center(child: Text(error.toString())),
+                  loading: buildLoadingIndicator,
+                ),
             if (selectionEnabledHook.value)
               ControlBottomAppBar(
                 onShare: onShareAssets,
@@ -301,7 +322,9 @@ class HomePage extends HookConsumerWidget {
                 albums: albums,
                 sharedAlbums: sharedAlbums,
                 onCreateNewAlbum: onCreateNewAlbum,
+                enabled: !processing.value,
               ),
+            if (processing.value) const Center(child: ImmichLoadingIndicator())
           ],
         ),
       );
diff --git a/mobile/lib/modules/login/providers/authentication.provider.dart b/mobile/lib/modules/login/providers/authentication.provider.dart
index 38b526c874..2b8cc88aec 100644
--- a/mobile/lib/modules/login/providers/authentication.provider.dart
+++ b/mobile/lib/modules/login/providers/authentication.provider.dart
@@ -9,13 +9,17 @@ import 'package:immich_mobile/shared/models/store.dart';
 import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
 import 'package:immich_mobile/shared/models/user.dart';
 import 'package:immich_mobile/shared/providers/api.provider.dart';
+import 'package:immich_mobile/shared/providers/db.provider.dart';
 import 'package:immich_mobile/shared/services/api.service.dart';
+import 'package:immich_mobile/utils/db.dart';
 import 'package:immich_mobile/utils/hash.dart';
+import 'package:isar/isar.dart';
 import 'package:openapi/api.dart';
 
 class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
   AuthenticationNotifier(
     this._apiService,
+    this._db,
   ) : super(
           AuthenticationState(
             deviceId: "",
@@ -31,6 +35,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
         );
 
   final ApiService _apiService;
+  final Isar _db;
 
   Future<bool> login(
     String email,
@@ -91,7 +96,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
     try {
       await Future.wait([
         _apiService.authenticationApi.logout(),
-        Store.delete(StoreKey.assetETag),
+        clearAssetsAndAlbums(_db),
         Store.delete(StoreKey.currentUser),
         Store.delete(StoreKey.accessToken),
       ]);
@@ -170,5 +175,6 @@ final authenticationProvider =
     StateNotifierProvider<AuthenticationNotifier, AuthenticationState>((ref) {
   return AuthenticationNotifier(
     ref.watch(apiServiceProvider),
+    ref.watch(dbProvider),
   );
 });
diff --git a/mobile/lib/modules/search/ui/search_result_grid.dart b/mobile/lib/modules/search/ui/search_result_grid.dart
index 14ccfb2949..2975999be4 100644
--- a/mobile/lib/modules/search/ui/search_result_grid.dart
+++ b/mobile/lib/modules/search/ui/search_result_grid.dart
@@ -8,6 +8,8 @@ class SearchResultGrid extends HookConsumerWidget {
 
   final List<Asset> assets;
 
+  Asset _loadAsset(int index) => assets[index];
+
   @override
   Widget build(BuildContext context, WidgetRef ref) {
     return GridView.builder(
@@ -22,7 +24,9 @@ class SearchResultGrid extends HookConsumerWidget {
         final asset = assets[index];
         return ThumbnailImage(
           asset: asset,
-          assetList: assets,
+          index: index,
+          loadAsset: _loadAsset,
+          totalAssets: assets.length,
           useGrayBoxPlaceholder: true,
         );
       },
diff --git a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_layout_settings.dart b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_layout_settings.dart
index dec1f09f4c..e378743355 100644
--- a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_layout_settings.dart
+++ b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_layout_settings.dart
@@ -5,7 +5,6 @@ import 'package:hooks_riverpod/hooks_riverpod.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/providers/asset.provider.dart';
 
 class LayoutSettings extends HookConsumerWidget {
   const LayoutSettings({
@@ -22,14 +21,17 @@ class LayoutSettings extends HookConsumerWidget {
     void switchChanged(bool value) {
       appSettingService.setSetting(AppSettingsEnum.dynamicLayout, value);
       useDynamicLayout.value = value;
-      ref.watch(assetProvider.notifier).rebuildAssetGridDataStructure();
+      ref.invalidate(appSettingsServiceProvider);
     }
 
     void changeGroupValue(GroupAssetsBy? value) {
       if (value != null) {
-        appSettingService.setSetting(AppSettingsEnum.groupAssetsBy, value.index);
+        appSettingService.setSetting(
+          AppSettingsEnum.groupAssetsBy,
+          value.index,
+        );
         groupBy.value = value;
-        ref.watch(assetProvider.notifier).rebuildAssetGridDataStructure();
+        ref.invalidate(appSettingsServiceProvider);
       }
     }
 
@@ -37,8 +39,8 @@ class LayoutSettings extends HookConsumerWidget {
       () {
         useDynamicLayout.value =
             appSettingService.getSetting<bool>(AppSettingsEnum.dynamicLayout);
-        groupBy.value =
-            GroupAssetsBy.values[appSettingService.getSetting<int>(AppSettingsEnum.groupAssetsBy)];
+        groupBy.value = GroupAssetsBy.values[
+            appSettingService.getSetting<int>(AppSettingsEnum.groupAssetsBy)];
 
         return null;
       },
@@ -93,6 +95,19 @@ class LayoutSettings extends HookConsumerWidget {
           onChanged: changeGroupValue,
           controlAffinity: ListTileControlAffinity.trailing,
         ),
+        RadioListTile(
+          activeColor: Theme.of(context).primaryColor,
+          title: const Text(
+            "asset_list_layout_settings_group_automatically",
+            style: TextStyle(
+              fontSize: 12,
+            ),
+          ).tr(),
+          value: GroupAssetsBy.auto,
+          groupValue: groupBy.value,
+          onChanged: changeGroupValue,
+          controlAffinity: ListTileControlAffinity.trailing,
+        ),
       ],
     );
   }
diff --git a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_storage_indicator.dart b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_storage_indicator.dart
index 9cd8ee5e9c..376ed35765 100644
--- a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_storage_indicator.dart
+++ b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_storage_indicator.dart
@@ -4,7 +4,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.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/providers/asset.provider.dart';
 
 class StorageIndicator extends HookConsumerWidget {
   const StorageIndicator({
@@ -20,12 +19,13 @@ class StorageIndicator extends HookConsumerWidget {
     void switchChanged(bool value) {
       appSettingService.setSetting(AppSettingsEnum.storageIndicator, value);
       showStorageIndicator.value = value;
-      ref.watch(assetProvider.notifier).rebuildAssetGridDataStructure();
+      ref.invalidate(appSettingsServiceProvider);
     }
 
     useEffect(
       () {
-        showStorageIndicator.value = appSettingService.getSetting<bool>(AppSettingsEnum.storageIndicator);
+        showStorageIndicator.value = appSettingService
+            .getSetting<bool>(AppSettingsEnum.storageIndicator);
 
         return null;
       },
diff --git a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_tiles_per_row.dart b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_tiles_per_row.dart
index 5e25dfce4f..eeb0f6c019 100644
--- a/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_tiles_per_row.dart
+++ b/mobile/lib/modules/settings/ui/asset_list_settings/asset_list_tiles_per_row.dart
@@ -4,7 +4,6 @@ import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.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/providers/asset.provider.dart';
 
 class TilesPerRow extends HookConsumerWidget {
   const TilesPerRow({
@@ -20,10 +19,7 @@ class TilesPerRow extends HookConsumerWidget {
     void sliderChanged(double value) {
       appSettingService.setSetting(AppSettingsEnum.tilesPerRow, value.toInt());
       itemsValue.value = value;
-    }
-
-    void sliderChangedEnd(double _) {
-      ref.watch(assetProvider.notifier).rebuildAssetGridDataStructure();
+      ref.invalidate(appSettingsServiceProvider);
     }
 
     useEffect(
@@ -49,7 +45,6 @@ class TilesPerRow extends HookConsumerWidget {
           ).tr(args: ["${itemsValue.value.toInt()}"]),
         ),
         Slider(
-          onChangeEnd: sliderChangedEnd,
           onChanged: sliderChanged,
           value: itemsValue.value,
           min: 2,
diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart
index b574068ceb..2a39ea6d7d 100644
--- a/mobile/lib/routing/router.gr.dart
+++ b/mobile/lib/routing/router.gr.dart
@@ -67,8 +67,9 @@ class _$AppRouter extends RootStackRouter {
         routeData: routeData,
         child: GalleryViewerPage(
           key: args.key,
-          assetList: args.assetList,
-          asset: args.asset,
+          initialIndex: args.initialIndex,
+          loadAsset: args.loadAsset,
+          totalAssets: args.totalAssets,
         ),
       );
     },
@@ -150,18 +151,27 @@ class _$AppRouter extends RootStackRouter {
       );
     },
     AssetSelectionRoute.name: (routeData) {
+      final args = routeData.argsAs<AssetSelectionRouteArgs>();
       return CustomPage<AssetSelectionPageResult?>(
         routeData: routeData,
-        child: const AssetSelectionPage(),
+        child: AssetSelectionPage(
+          key: args.key,
+          existingAssets: args.existingAssets,
+          isNewAlbum: args.isNewAlbum,
+        ),
         transitionsBuilder: TransitionsBuilders.slideBottom,
         opaque: true,
         barrierDismissible: false,
       );
     },
     SelectUserForSharingRoute.name: (routeData) {
+      final args = routeData.argsAs<SelectUserForSharingRouteArgs>();
       return CustomPage<List<String>>(
         routeData: routeData,
-        child: const SelectUserForSharingPage(),
+        child: SelectUserForSharingPage(
+          key: args.key,
+          assets: args.assets,
+        ),
         transitionsBuilder: TransitionsBuilders.slideBottom,
         opaque: true,
         barrierDismissible: false,
@@ -582,15 +592,17 @@ class TabControllerRoute extends PageRouteInfo<void> {
 class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
   GalleryViewerRoute({
     Key? key,
-    required List<Asset> assetList,
-    required Asset asset,
+    required int initialIndex,
+    required Asset Function(int) loadAsset,
+    required int totalAssets,
   }) : super(
           GalleryViewerRoute.name,
           path: '/gallery-viewer-page',
           args: GalleryViewerRouteArgs(
             key: key,
-            assetList: assetList,
-            asset: asset,
+            initialIndex: initialIndex,
+            loadAsset: loadAsset,
+            totalAssets: totalAssets,
           ),
         );
 
@@ -600,19 +612,22 @@ class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
 class GalleryViewerRouteArgs {
   const GalleryViewerRouteArgs({
     this.key,
-    required this.assetList,
-    required this.asset,
+    required this.initialIndex,
+    required this.loadAsset,
+    required this.totalAssets,
   });
 
   final Key? key;
 
-  final List<Asset> assetList;
+  final int initialIndex;
 
-  final Asset asset;
+  final Asset Function(int) loadAsset;
+
+  final int totalAssets;
 
   @override
   String toString() {
-    return 'GalleryViewerRouteArgs{key: $key, assetList: $assetList, asset: $asset}';
+    return 'GalleryViewerRouteArgs{key: $key, initialIndex: $initialIndex, loadAsset: $loadAsset, totalAssets: $totalAssets}';
   }
 }
 
@@ -623,9 +638,9 @@ class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> {
     Key? key,
     required Asset asset,
     required bool isMotionVideo,
-    required void Function() onVideoEnded,
-    void Function()? onPlaying,
-    void Function()? onPaused,
+    required dynamic onVideoEnded,
+    dynamic onPlaying,
+    dynamic onPaused,
   }) : super(
           VideoViewerRoute.name,
           path: '/video-viewer-page',
@@ -658,11 +673,11 @@ class VideoViewerRouteArgs {
 
   final bool isMotionVideo;
 
-  final void Function() onVideoEnded;
+  final dynamic onVideoEnded;
 
-  final void Function()? onPlaying;
+  final dynamic onPlaying;
 
-  final void Function()? onPaused;
+  final dynamic onPaused;
 
   @override
   String toString() {
@@ -829,28 +844,78 @@ class RecentlyAddedRoute extends PageRouteInfo<void> {
 
 /// generated route for
 /// [AssetSelectionPage]
-class AssetSelectionRoute extends PageRouteInfo<void> {
-  const AssetSelectionRoute()
-      : super(
+class AssetSelectionRoute extends PageRouteInfo<AssetSelectionRouteArgs> {
+  AssetSelectionRoute({
+    Key? key,
+    required Set<Asset> existingAssets,
+    bool isNewAlbum = false,
+  }) : super(
           AssetSelectionRoute.name,
           path: '/asset-selection-page',
+          args: AssetSelectionRouteArgs(
+            key: key,
+            existingAssets: existingAssets,
+            isNewAlbum: isNewAlbum,
+          ),
         );
 
   static const String name = 'AssetSelectionRoute';
 }
 
+class AssetSelectionRouteArgs {
+  const AssetSelectionRouteArgs({
+    this.key,
+    required this.existingAssets,
+    this.isNewAlbum = false,
+  });
+
+  final Key? key;
+
+  final Set<Asset> existingAssets;
+
+  final bool isNewAlbum;
+
+  @override
+  String toString() {
+    return 'AssetSelectionRouteArgs{key: $key, existingAssets: $existingAssets, isNewAlbum: $isNewAlbum}';
+  }
+}
+
 /// generated route for
 /// [SelectUserForSharingPage]
-class SelectUserForSharingRoute extends PageRouteInfo<void> {
-  const SelectUserForSharingRoute()
-      : super(
+class SelectUserForSharingRoute
+    extends PageRouteInfo<SelectUserForSharingRouteArgs> {
+  SelectUserForSharingRoute({
+    Key? key,
+    required Set<Asset> assets,
+  }) : super(
           SelectUserForSharingRoute.name,
           path: '/select-user-for-sharing-page',
+          args: SelectUserForSharingRouteArgs(
+            key: key,
+            assets: assets,
+          ),
         );
 
   static const String name = 'SelectUserForSharingRoute';
 }
 
+class SelectUserForSharingRouteArgs {
+  const SelectUserForSharingRouteArgs({
+    this.key,
+    required this.assets,
+  });
+
+  final Key? key;
+
+  final Set<Asset> assets;
+
+  @override
+  String toString() {
+    return 'SelectUserForSharingRouteArgs{key: $key, assets: $assets}';
+  }
+}
+
 /// generated route for
 /// [AlbumViewerPage]
 class AlbumViewerRoute extends PageRouteInfo<AlbumViewerRouteArgs> {
diff --git a/mobile/lib/shared/models/album.dart b/mobile/lib/shared/models/album.dart
index 553c8f0548..508ab094aa 100644
--- a/mobile/lib/shared/models/album.dart
+++ b/mobile/lib/shared/models/album.dart
@@ -1,4 +1,5 @@
 import 'package:flutter/cupertino.dart';
+import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
 import 'package:immich_mobile/shared/models/store.dart';
 import 'package:immich_mobile/shared/models/user.dart';
@@ -34,10 +35,10 @@ class Album {
   final IsarLinks<User> sharedUsers = IsarLinks<User>();
   final IsarLinks<Asset> assets = IsarLinks<Asset>();
 
-  List<Asset> _sortedAssets = [];
+  RenderList _renderList = RenderList.empty();
 
   @ignore
-  List<Asset> get sortedAssets => _sortedAssets;
+  RenderList get renderList => _renderList;
 
   @ignore
   bool get isRemote => remoteId != null;
@@ -69,8 +70,14 @@ class Album {
     return name.join(' ');
   }
 
-  Future<void> loadSortedAssets() async {
-    _sortedAssets = await assets.filter().sortByFileCreatedAt().findAll();
+  Stream<void> watchRenderList(GroupAssetsBy groupAssetsBy) async* {
+    final query = assets.filter().sortByFileCreatedAt();
+    _renderList = await RenderList.fromQuery(query, groupAssetsBy);
+    yield _renderList;
+    await for (final _ in query.watchLazy()) {
+      _renderList = await RenderList.fromQuery(query, groupAssetsBy);
+      yield _renderList;
+    }
   }
 
   @override
diff --git a/mobile/lib/shared/models/asset.dart b/mobile/lib/shared/models/asset.dart
index a2e23268c9..d8fedcd6b2 100644
--- a/mobile/lib/shared/models/asset.dart
+++ b/mobile/lib/shared/models/asset.dart
@@ -225,7 +225,6 @@ class Asset {
         a.isLocal && !isLocal ||
         width == null && a.width != null ||
         height == null && a.height != null ||
-        exifInfo == null && a.exifInfo != null ||
         livePhotoVideoId == null && a.livePhotoVideoId != null ||
         !isRemote && a.isRemote && isFavorite != a.isFavorite ||
         !isRemote && a.isRemote && isArchived != a.isArchived;
diff --git a/mobile/lib/shared/models/exif_info.dart b/mobile/lib/shared/models/exif_info.dart
index c694168dbe..4609c0487c 100644
--- a/mobile/lib/shared/models/exif_info.dart
+++ b/mobile/lib/shared/models/exif_info.dart
@@ -114,6 +114,45 @@ class ExifInfo {
         country: country ?? this.country,
         description: description ?? this.description,
       );
+
+  @override
+  bool operator ==(other) {
+    if (other is! ExifInfo) return false;
+    return id == other.id &&
+        fileSize == other.fileSize &&
+        make == other.make &&
+        model == other.model &&
+        lens == other.lens &&
+        f == other.f &&
+        mm == other.mm &&
+        iso == other.iso &&
+        exposureSeconds == other.exposureSeconds &&
+        lat == other.lat &&
+        long == other.long &&
+        city == other.city &&
+        state == other.state &&
+        country == other.country &&
+        description == other.description;
+  }
+
+  @override
+  @ignore
+  int get hashCode =>
+      id.hashCode ^
+      fileSize.hashCode ^
+      make.hashCode ^
+      model.hashCode ^
+      lens.hashCode ^
+      f.hashCode ^
+      mm.hashCode ^
+      iso.hashCode ^
+      exposureSeconds.hashCode ^
+      lat.hashCode ^
+      long.hashCode ^
+      city.hashCode ^
+      state.hashCode ^
+      country.hashCode ^
+      description.hashCode;
 }
 
 double? _exposureTimeToSeconds(String? s) {
diff --git a/mobile/lib/shared/models/store.dart b/mobile/lib/shared/models/store.dart
index 201ffc5a2d..7a07e62719 100644
--- a/mobile/lib/shared/models/store.dart
+++ b/mobile/lib/shared/models/store.dart
@@ -35,6 +35,10 @@ class Store {
     return value;
   }
 
+  /// Watches a specific key for changes
+  static Stream<T?> watch<T>(StoreKey<T> key) =>
+      _db.storeValues.watchObject(key.id).map((e) => e?._extract(key));
+
   /// Returns the stored value for the given key (possibly null)
   static T? tryGet<T>(StoreKey<T> key) => _cache[key.id];
 
diff --git a/mobile/lib/shared/providers/asset.provider.dart b/mobile/lib/shared/providers/asset.provider.dart
index 02eabded3a..4b422ecd7a 100644
--- a/mobile/lib/shared/providers/asset.provider.dart
+++ b/mobile/lib/shared/providers/asset.provider.dart
@@ -3,18 +3,14 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/modules/album/services/album.service.dart';
 import 'package:immich_mobile/shared/models/exif_info.dart';
 import 'package:immich_mobile/shared/models/store.dart';
-import 'package:immich_mobile/shared/models/user.dart';
 import 'package:immich_mobile/shared/providers/db.provider.dart';
 import 'package:immich_mobile/shared/services/asset.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:collection/collection.dart';
 import 'package:immich_mobile/shared/services/sync.service.dart';
-import 'package:immich_mobile/utils/async_mutex.dart';
 import 'package:immich_mobile/utils/db.dart';
-import 'package:intl/intl.dart';
 import 'package:isar/isar.dart';
 import 'package:logging/logging.dart';
 import 'package:openapi/api.dart';
@@ -22,72 +18,23 @@ import 'package:photo_manager/photo_manager.dart';
 
 /// State does not contain archived assets.
 /// Use database provider if you want to access the isArchived assets
-class AssetsState {
-  final List<Asset> allAssets;
-  final RenderList? renderList;
-
-  AssetsState(this.allAssets, {this.renderList});
-
-  Future<AssetsState> withRenderDataStructure(
-    AssetGridLayoutParameters layout,
-  ) async {
-    return AssetsState(
-      allAssets,
-      renderList: await RenderList.fromAssets(
-        allAssets,
-        layout,
-      ),
-    );
-  }
-
-  AssetsState withAdditionalAssets(List<Asset> toAdd) {
-    return AssetsState([...allAssets, ...toAdd]);
-  }
-
-  static AssetsState fromAssetList(List<Asset> assets) {
-    return AssetsState(assets);
-  }
-
-  static AssetsState empty() {
-    return AssetsState([]);
-  }
-}
+class AssetsState {}
 
 class AssetNotifier extends StateNotifier<AssetsState> {
   final AssetService _assetService;
-  final AppSettingsService _settingsService;
   final AlbumService _albumService;
   final SyncService _syncService;
   final Isar _db;
   final log = Logger('AssetNotifier');
   bool _getAllAssetInProgress = false;
   bool _deleteInProgress = false;
-  final AsyncMutex _stateUpdateLock = AsyncMutex();
 
   AssetNotifier(
     this._assetService,
-    this._settingsService,
     this._albumService,
     this._syncService,
     this._db,
-  ) : super(AssetsState.fromAssetList([]));
-
-  Future<void> _updateAssetsState(List<Asset> newAssetList) async {
-    final layout = AssetGridLayoutParameters(
-      _settingsService.getSetting(AppSettingsEnum.tilesPerRow),
-      _settingsService.getSetting(AppSettingsEnum.dynamicLayout),
-      GroupAssetsBy
-          .values[_settingsService.getSetting(AppSettingsEnum.groupAssetsBy)],
-    );
-
-    state = await AssetsState.fromAssetList(newAssetList)
-        .withRenderDataStructure(layout);
-  }
-
-  // Just a little helper to trigger a rebuild of the state object
-  Future<void> rebuildAssetGridDataStructure() async {
-    await _updateAssetsState(state.allAssets);
-  }
+  ) : super(AssetsState());
 
   Future<void> getAllAsset({bool clear = false}) async {
     if (_getAllAssetInProgress || _deleteInProgress) {
@@ -97,79 +44,32 @@ class AssetNotifier extends StateNotifier<AssetsState> {
     final stopwatch = Stopwatch()..start();
     try {
       _getAllAssetInProgress = true;
-      final User me = Store.get(StoreKey.currentUser);
       if (clear) {
         await clearAssetsAndAlbums(_db);
         log.info("Manual refresh requested, cleared assets and albums from db");
-      } else if (_stateUpdateLock.enqueued <= 1) {
-        final int cachedCount = await _userAssetQuery(me.isarId).count();
-        if (cachedCount > 0 && cachedCount != state.allAssets.length) {
-          await _stateUpdateLock.run(
-            () async => _updateAssetsState(await _getUserAssets(me.isarId)),
-          );
-          log.info(
-            "Reading assets ${state.allAssets.length} from DB: ${stopwatch.elapsedMilliseconds}ms",
-          );
-          stopwatch.reset();
-        }
       }
       final bool newRemote = await _assetService.refreshRemoteAssets();
       final bool newLocal = await _albumService.refreshDeviceAlbums();
       debugPrint("newRemote: $newRemote, newLocal: $newLocal");
       log.info("Load assets: ${stopwatch.elapsedMilliseconds}ms");
-      stopwatch.reset();
-      if (!newRemote &&
-          !newLocal &&
-          state.allAssets.length == await _userAssetQuery(me.isarId).count()) {
-        log.info("state is already up-to-date");
-        return;
-      }
-      stopwatch.reset();
-      if (_stateUpdateLock.enqueued <= 1) {
-        _stateUpdateLock.run(() async {
-          final assets = await _getUserAssets(me.isarId);
-          if (!const ListEquality().equals(assets, state.allAssets)) {
-            log.info("setting new asset state");
-            await _updateAssetsState(assets);
-          }
-        });
-      }
     } finally {
       _getAllAssetInProgress = false;
     }
   }
 
-  Future<List<Asset>> _getUserAssets(int userId) =>
-      _userAssetQuery(userId).sortByFileCreatedAtDesc().findAll();
-
-  QueryBuilder<Asset, Asset, QAfterFilterCondition> _userAssetQuery(
-    int userId,
-  ) =>
-      _db.assets.filter().ownerIdEqualTo(userId).isArchivedEqualTo(false);
-
   Future<void> clearAllAsset() {
-    state = AssetsState.empty();
     return clearAssetsAndAlbums(_db);
   }
 
   Future<void> onNewAssetUploaded(Asset newAsset) async {
-    final bool ok = await _syncService.syncNewAssetToDb(newAsset);
-    if (ok && _stateUpdateLock.enqueued <= 1) {
-      // run this sequentially if there is at most 1 other task waiting
-      await _stateUpdateLock.run(() async {
-        final userId = Store.get(StoreKey.currentUser).isarId;
-        final assets = await _getUserAssets(userId);
-        await _updateAssetsState(assets);
-      });
-    }
+    // eTag on device is not valid after partially modifying the assets
+    Store.delete(StoreKey.assetETag);
+    await _syncService.syncNewAssetToDb(newAsset);
   }
 
   Future<void> deleteAssets(Set<Asset> deleteAssets) async {
     _deleteInProgress = true;
     try {
-      _updateAssetsState(
-        state.allAssets.whereNot(deleteAssets.contains).toList(),
-      );
       final localDeleted = await _deleteLocalAssets(deleteAssets);
       final remoteDeleted = await _deleteRemoteAssets(deleteAssets);
       if (localDeleted.isNotEmpty || remoteDeleted.isNotEmpty) {
@@ -201,7 +101,7 @@ class AssetNotifier extends StateNotifier<AssetsState> {
     }
     if (local.isNotEmpty) {
       try {
-        await PhotoManager.editor.deleteWithIds(local);
+        return await PhotoManager.editor.deleteWithIds(local);
       } catch (e, stack) {
         log.severe("Failed to delete asset from device", e, stack);
       }
@@ -220,53 +120,25 @@ class AssetNotifier extends StateNotifier<AssetsState> {
         .map((a) => a.id);
   }
 
-  Future<bool> toggleFavorite(Asset asset, bool status) async {
-    final newAsset = await _assetService.changeFavoriteStatus(asset, status);
-
-    if (newAsset == null) {
-      log.severe("Change favorite status failed for asset ${asset.id}");
-      return asset.isFavorite;
+  Future<void> toggleFavorite(List<Asset> assets, bool status) async {
+    final newAssets = await _assetService.changeFavoriteStatus(assets, status);
+    for (Asset? newAsset in newAssets) {
+      if (newAsset == null) {
+        log.severe("Change favorite status failed for asset");
+        continue;
+      }
     }
-
-    final index = state.allAssets.indexWhere((a) => asset.id == a.id);
-    if (index != -1) {
-      state.allAssets[index] = newAsset;
-      _updateAssetsState(state.allAssets);
-    }
-
-    return newAsset.isFavorite;
   }
 
-  Future<void> toggleArchive(Iterable<Asset> assets, bool status) async {
-    final newAssets = await Future.wait(
-      assets.map((a) => _assetService.changeArchiveStatus(a, status)),
-    );
+  Future<void> toggleArchive(List<Asset> assets, bool status) async {
+    final newAssets = await _assetService.changeArchiveStatus(assets, status);
     int i = 0;
-    bool unArchived = false;
     for (Asset oldAsset in assets) {
       final newAsset = newAssets[i++];
       if (newAsset == null) {
         log.severe("Change archive status failed for asset ${oldAsset.id}");
         continue;
       }
-      final index = state.allAssets.indexWhere((a) => oldAsset.id == a.id);
-      if (newAsset.isArchived) {
-        // remove from state
-        if (index != -1) {
-          state.allAssets.removeAt(index);
-        }
-      } else {
-        // add to state is difficult because the list is sorted
-        unArchived = true;
-      }
-    }
-    if (unArchived) {
-      final User me = Store.get(StoreKey.currentUser);
-      await _stateUpdateLock.run(
-        () async => _updateAssetsState(await _getUserAssets(me.isarId)),
-      );
-    } else {
-      _updateAssetsState(state.allAssets);
     }
   }
 }
@@ -274,26 +146,53 @@ class AssetNotifier extends StateNotifier<AssetsState> {
 final assetProvider = StateNotifierProvider<AssetNotifier, AssetsState>((ref) {
   return AssetNotifier(
     ref.watch(assetServiceProvider),
-    ref.watch(appSettingsServiceProvider),
     ref.watch(albumServiceProvider),
     ref.watch(syncServiceProvider),
     ref.watch(dbProvider),
   );
 });
 
-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).allAssets.where((e) => e.isRemote).toList();
-
-  assets.sortByCompare<DateTime>(
-    (e) => e.fileCreatedAt,
-    (a, b) => b.compareTo(a),
-  );
-
-  return assets.groupListsBy(
-    (element) => DateFormat('MMMM, y').format(element.fileCreatedAt.toLocal()),
-  );
+final assetDetailProvider =
+    StreamProvider.autoDispose.family<Asset, Asset>((ref, asset) async* {
+  yield await ref.watch(assetServiceProvider).loadExif(asset);
+  final db = ref.watch(dbProvider);
+  await for (final a in db.assets.watchObject(asset.id)) {
+    if (a != null) yield await ref.watch(assetServiceProvider).loadExif(a);
+  }
+});
+
+final assetsProvider = StreamProvider.autoDispose<RenderList>((ref) async* {
+  final query = ref
+      .watch(dbProvider)
+      .assets
+      .filter()
+      .ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
+      .isArchivedEqualTo(false)
+      .sortByFileCreatedAtDesc();
+  final settings = ref.watch(appSettingsServiceProvider);
+  final groupBy =
+      GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)];
+  yield await RenderList.fromQuery(query, groupBy);
+  await for (final _ in query.watchLazy()) {
+    yield await RenderList.fromQuery(query, groupBy);
+  }
+});
+
+final remoteAssetsProvider =
+    StreamProvider.autoDispose<RenderList>((ref) async* {
+  final query = ref
+      .watch(dbProvider)
+      .assets
+      .where()
+      .remoteIdIsNotNull()
+      .filter()
+      .ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId)
+      .sortByFileCreatedAt();
+  final settings = ref.watch(appSettingsServiceProvider);
+  final groupBy =
+      GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)];
+  yield await RenderList.fromQuery(query, groupBy);
+  await for (final _ in query.watchLazy()) {
+    yield await RenderList.fromQuery(query, groupBy);
+  }
 });
diff --git a/mobile/lib/shared/services/asset.service.dart b/mobile/lib/shared/services/asset.service.dart
index 8ba88b48f4..f7eb2a066d 100644
--- a/mobile/lib/shared/services/asset.service.dart
+++ b/mobile/lib/shared/services/asset.service.dart
@@ -97,15 +97,18 @@ class AssetService {
   /// the exif info from the server (remote assets only)
   Future<Asset> loadExif(Asset a) async {
     a.exifInfo ??= await _db.exifInfos.get(a.id);
-    if (a.exifInfo?.iso == null) {
+    // fileSize is always filled on the server but not set on client
+    if (a.exifInfo?.fileSize == null) {
       if (a.isRemote) {
         final dto = await _apiService.assetApi.getAssetById(a.remoteId!);
         if (dto != null && dto.exifInfo != null) {
-          a.exifInfo = Asset.remote(dto).exifInfo!.copyWith(id: a.id);
-          if (a.isInDb) {
-            _db.writeTxn(() => a.put(_db));
-          } else {
-            debugPrint("[loadExif] parameter Asset is not from DB!");
+          final newExif = Asset.remote(dto).exifInfo!.copyWith(id: a.id);
+          if (newExif != a.exifInfo) {
+            if (a.isInDb) {
+              _db.writeTxn(() => a.put(_db));
+            } else {
+              debugPrint("[loadExif] parameter Asset is not from DB!");
+            }
           }
         }
       } else {
@@ -115,27 +118,39 @@ class AssetService {
     return a;
   }
 
-  Future<Asset?> updateAsset(
-    Asset asset,
+  Future<List<Asset?>> updateAssets(
+    List<Asset> assets,
     UpdateAssetDto updateAssetDto,
   ) async {
-    final dto =
-        await _apiService.assetApi.updateAsset(asset.remoteId!, updateAssetDto);
-    if (dto != null) {
-      final updated = asset.updatedCopy(Asset.remote(dto));
-      if (updated.isInDb) {
-        await _db.writeTxn(() => updated.put(_db));
+    final List<AssetResponseDto?> dtos = await Future.wait(
+      assets.map(
+        (a) => _apiService.assetApi.updateAsset(a.remoteId!, updateAssetDto),
+      ),
+    );
+    bool allInDb = true;
+    for (int i = 0; i < assets.length; i++) {
+      final dto = dtos[i], old = assets[i];
+      if (dto != null) {
+        final remote = Asset.remote(dto);
+        if (old.canUpdate(remote)) {
+          assets[i] = old.updatedCopy(remote);
+        }
+        allInDb &= assets[i].isInDb;
       }
-      return updated;
     }
-    return null;
+    final toUpdate = allInDb ? assets : assets.where((e) => e.isInDb).toList();
+    await _syncService.upsertAssetsWithExif(toUpdate);
+    return assets;
   }
 
-  Future<Asset?> changeFavoriteStatus(Asset asset, bool isFavorite) {
-    return updateAsset(asset, UpdateAssetDto(isFavorite: isFavorite));
+  Future<List<Asset?>> changeFavoriteStatus(
+    List<Asset> assets,
+    bool isFavorite,
+  ) {
+    return updateAssets(assets, UpdateAssetDto(isFavorite: isFavorite));
   }
 
-  Future<Asset?> changeArchiveStatus(Asset asset, bool isArchive) {
-    return updateAsset(asset, UpdateAssetDto(isArchived: isArchive));
+  Future<List<Asset?>> changeArchiveStatus(List<Asset> assets, bool isArchive) {
+    return updateAssets(assets, UpdateAssetDto(isArchived: isArchive));
   }
 }
diff --git a/mobile/lib/shared/services/sync.service.dart b/mobile/lib/shared/services/sync.service.dart
index 664045c2fe..efd0954ee5 100644
--- a/mobile/lib/shared/services/sync.service.dart
+++ b/mobile/lib/shared/services/sync.service.dart
@@ -172,7 +172,7 @@ class SyncService {
     final idsToDelete = diff.third.map((e) => e.id).toList();
     try {
       await _db.writeTxn(() => _db.assets.deleteAll(idsToDelete));
-      await _upsertAssetsWithExif(diff.first + diff.second);
+      await upsertAssetsWithExif(diff.first + diff.second);
     } on IsarError catch (e) {
       _log.severe("Failed to sync remote assets to db: $e");
     }
@@ -272,7 +272,7 @@ class SyncService {
 
     // for shared album: put missing album assets into local DB
     final resultPair = await _linkWithExistingFromDb(toAdd);
-    await _upsertAssetsWithExif(resultPair.second);
+    await upsertAssetsWithExif(resultPair.second);
     final assetsToLink = resultPair.first + resultPair.second;
     final usersToLink = (await _db.users.getAllById(userIdsToAdd)).cast<User>();
 
@@ -329,7 +329,7 @@ class SyncService {
       // put missing album assets into local DB
       final result = await _linkWithExistingFromDb(dto.getAssets());
       existing.addAll(result.first);
-      await _upsertAssetsWithExif(result.second);
+      await upsertAssetsWithExif(result.second);
 
       final Album a = await Album.remote(dto);
       await _db.writeTxn(() => _db.albums.store(a));
@@ -540,7 +540,7 @@ class SyncService {
     _log.info(
       "${result.first.length} assets already existed in DB, to upsert ${result.second.length}",
     );
-    await _upsertAssetsWithExif(result.second);
+    await upsertAssetsWithExif(result.second);
     existing.addAll(result.first);
     a.assets.addAll(result.first);
     a.assets.addAll(result.second);
@@ -600,7 +600,7 @@ class SyncService {
   }
 
   /// Inserts or updates the assets in the database with their ExifInfo (if any)
-  Future<void> _upsertAssetsWithExif(List<Asset> assets) async {
+  Future<void> upsertAssetsWithExif(List<Asset> assets) async {
     if (assets.isEmpty) {
       return;
     }
diff --git a/mobile/lib/shared/ui/drag_sheet.dart b/mobile/lib/shared/ui/drag_sheet.dart
index 1b57a1909e..574962cc06 100644
--- a/mobile/lib/shared/ui/drag_sheet.dart
+++ b/mobile/lib/shared/ui/drag_sheet.dart
@@ -21,19 +21,19 @@ class ControlBoxButton extends StatelessWidget {
     Key? key,
     required this.label,
     required this.iconData,
-    required this.onPressed,
+    this.onPressed,
   }) : super(key: key);
 
   final String label;
   final IconData iconData;
-  final Function onPressed;
+  final void Function()? onPressed;
 
   @override
   Widget build(BuildContext context) {
     return MaterialButton(
       padding: const EdgeInsets.all(10),
       shape: const CircleBorder(),
-      onPressed: () => onPressed(),
+      onPressed: onPressed,
       child: Column(
         mainAxisAlignment: MainAxisAlignment.start,
         crossAxisAlignment: CrossAxisAlignment.center,
diff --git a/mobile/lib/utils/builtin_extensions.dart b/mobile/lib/utils/builtin_extensions.dart
index 9be774c527..3a3a723dc1 100644
--- a/mobile/lib/utils/builtin_extensions.dart
+++ b/mobile/lib/utils/builtin_extensions.dart
@@ -1,3 +1,5 @@
+import 'package:collection/collection.dart';
+
 extension DurationExtension on String {
   Duration? toDuration() {
     try {
@@ -34,4 +36,12 @@ extension ListExtension<E> on List<E> {
     length = length == 0 ? 0 : j;
     return this;
   }
+
+  ListSlice<E> nestedSlice(int start, int end) {
+    if (this is ListSlice) {
+      final ListSlice<E> self = this as ListSlice<E>;
+      return ListSlice<E>(self.source, self.start + start, self.start + end);
+    }
+    return ListSlice<E>(this, start, end);
+  }
 }
diff --git a/mobile/test/asset_grid_data_structure_test.dart b/mobile/test/asset_grid_data_structure_test.dart
index 875abe1fdd..78c553f5a8 100644
--- a/mobile/test/asset_grid_data_structure_test.dart
+++ b/mobile/test/asset_grid_data_structure_test.dart
@@ -60,11 +60,7 @@ void main() {
     test('test grouped check months', () async {
       final renderList = await RenderList.fromAssets(
         assets,
-        AssetGridLayoutParameters(
-          3,
-          false,
-          GroupAssetsBy.day,
-        ),
+        GroupAssetsBy.day,
       );
 
       // Oct
@@ -78,32 +74,33 @@ void main() {
       // 5 Assets => 2 Rows
       // Day 1
       // 5 Assets => 2 Rows
-      expect(renderList.elements.length, 18);
+      expect(renderList.elements.length, 4);
       expect(
         renderList.elements[0].type,
         RenderAssetGridElementType.monthTitle,
       );
-      expect(renderList.elements[0].date.month, 10);
+      expect(renderList.elements[0].date.month, 1);
       expect(
-        renderList.elements[7].type,
+        renderList.elements[1].type,
+        RenderAssetGridElementType.groupDividerTitle,
+      );
+      expect(renderList.elements[1].date.month, 1);
+      expect(
+        renderList.elements[2].type,
         RenderAssetGridElementType.monthTitle,
       );
-      expect(renderList.elements[7].date.month, 2);
+      expect(renderList.elements[2].date.month, 2);
       expect(
-        renderList.elements[11].type,
+        renderList.elements[3].type,
         RenderAssetGridElementType.monthTitle,
       );
-      expect(renderList.elements[11].date.month, 1);
+      expect(renderList.elements[3].date.month, 10);
     });
 
     test('test grouped check types', () async {
       final renderList = await RenderList.fromAssets(
         assets,
-        AssetGridLayoutParameters(
-          5,
-          false,
-          GroupAssetsBy.day,
-        ),
+        GroupAssetsBy.day,
       );
 
       // Oct
@@ -120,17 +117,8 @@ void main() {
       final types = [
         RenderAssetGridElementType.monthTitle,
         RenderAssetGridElementType.groupDividerTitle,
-        RenderAssetGridElementType.assetRow,
-        RenderAssetGridElementType.assetRow,
-        RenderAssetGridElementType.assetRow,
         RenderAssetGridElementType.monthTitle,
-        RenderAssetGridElementType.groupDividerTitle,
-        RenderAssetGridElementType.assetRow,
         RenderAssetGridElementType.monthTitle,
-        RenderAssetGridElementType.groupDividerTitle,
-        RenderAssetGridElementType.assetRow,
-        RenderAssetGridElementType.groupDividerTitle,
-        RenderAssetGridElementType.assetRow,
       ];
 
       expect(renderList.elements.length, types.length);
diff --git a/mobile/test/favorite_provider_test.dart b/mobile/test/favorite_provider_test.dart
deleted file mode 100644
index 505187a7d7..0000000000
--- a/mobile/test/favorite_provider_test.dart
+++ /dev/null
@@ -1,112 +0,0 @@
-import 'package:flutter_test/flutter_test.dart';
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
-import 'package:immich_mobile/shared/models/asset.dart';
-import 'package:immich_mobile/shared/providers/asset.provider.dart';
-import 'package:mockito/annotations.dart';
-import 'package:mockito/mockito.dart';
-
-@GenerateNiceMocks([
-  MockSpec<AssetsState>(),
-  MockSpec<AssetNotifier>(),
-])
-import 'favorite_provider_test.mocks.dart';
-
-Asset _getTestAsset(int id, bool favorite) {
-  final Asset a = Asset(
-    remoteId: id.toString(),
-    localId: id.toString(),
-    deviceId: 1,
-    ownerId: 1,
-    fileCreatedAt: DateTime.now(),
-    fileModifiedAt: DateTime.now(),
-    updatedAt: DateTime.now(),
-    isLocal: false,
-    durationInSeconds: 0,
-    type: AssetType.image,
-    fileName: '',
-    isFavorite: favorite,
-    isArchived: false,
-  );
-  a.id = id;
-  return a;
-}
-
-void main() {
-  group("Test favoriteProvider", () {
-    late MockAssetsState assetsState;
-    late MockAssetNotifier assetNotifier;
-    late ProviderContainer container;
-    late StateNotifierProvider<FavoriteSelectionNotifier, Set<int>>
-        testFavoritesProvider;
-
-    setUp(
-      () {
-        assetsState = MockAssetsState();
-        assetNotifier = MockAssetNotifier();
-        container = ProviderContainer();
-
-        testFavoritesProvider =
-            StateNotifierProvider<FavoriteSelectionNotifier, Set<int>>((ref) {
-          return FavoriteSelectionNotifier(
-            assetsState,
-            assetNotifier,
-          );
-        });
-      },
-    );
-
-    test("Empty favorites provider", () {
-      when(assetsState.allAssets).thenReturn([]);
-      expect(<int>{}, container.read(testFavoritesProvider));
-    });
-
-    test("Non-empty favorites provider", () {
-      when(assetsState.allAssets).thenReturn([
-        _getTestAsset(1, false),
-        _getTestAsset(2, true),
-        _getTestAsset(3, false),
-        _getTestAsset(4, false),
-        _getTestAsset(5, true),
-      ]);
-
-      expect(<int>{2, 5}, container.read(testFavoritesProvider));
-    });
-
-    test("Toggle favorite", () {
-      when(assetNotifier.toggleFavorite(null, false))
-          .thenAnswer((_) async => false);
-
-      final testAsset1 = _getTestAsset(1, false);
-      final testAsset2 = _getTestAsset(2, true);
-
-      when(assetsState.allAssets).thenReturn([testAsset1, testAsset2]);
-
-      expect(<int>{2}, container.read(testFavoritesProvider));
-
-      container.read(testFavoritesProvider.notifier).toggleFavorite(testAsset2);
-      expect(<int>{}, container.read(testFavoritesProvider));
-
-      container.read(testFavoritesProvider.notifier).toggleFavorite(testAsset1);
-      expect(<int>{1}, container.read(testFavoritesProvider));
-    });
-
-    test("Add favorites", () {
-      when(assetNotifier.toggleFavorite(null, false))
-          .thenAnswer((_) async => false);
-
-      when(assetsState.allAssets).thenReturn([]);
-
-      expect(<int>{}, container.read(testFavoritesProvider));
-
-      container.read(testFavoritesProvider.notifier).addToFavorites(
-        [
-          _getTestAsset(1, false),
-          _getTestAsset(2, false),
-        ],
-      );
-
-      expect(<int>{1, 2}, container.read(testFavoritesProvider));
-    });
-  });
-}
diff --git a/mobile/test/favorite_provider_test.mocks.dart b/mobile/test/favorite_provider_test.mocks.dart
deleted file mode 100644
index 569c12e791..0000000000
--- a/mobile/test/favorite_provider_test.mocks.dart
+++ /dev/null
@@ -1,298 +0,0 @@
-// Mocks generated by Mockito 5.3.2 from annotations
-// in immich_mobile/test/favorite_provider_test.dart.
-// Do not manually edit this file.
-
-// ignore_for_file: no_leading_underscores_for_library_prefixes
-import 'dart:async' as _i5;
-
-import 'package:hooks_riverpod/hooks_riverpod.dart' as _i7;
-import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'
-    as _i6;
-import 'package:immich_mobile/shared/models/asset.dart' as _i4;
-import 'package:immich_mobile/shared/providers/asset.provider.dart' as _i2;
-import 'package:logging/logging.dart' as _i3;
-import 'package:mockito/mockito.dart' as _i1;
-import 'package:state_notifier/state_notifier.dart' as _i8;
-
-// ignore_for_file: type=lint
-// ignore_for_file: avoid_redundant_argument_values
-// ignore_for_file: avoid_setters_without_getters
-// ignore_for_file: comment_references
-// ignore_for_file: implementation_imports
-// ignore_for_file: invalid_use_of_visible_for_testing_member
-// ignore_for_file: prefer_const_constructors
-// ignore_for_file: unnecessary_parenthesis
-// ignore_for_file: camel_case_types
-// ignore_for_file: subtype_of_sealed_class
-
-class _FakeAssetsState_0 extends _i1.SmartFake implements _i2.AssetsState {
-  _FakeAssetsState_0(
-    Object parent,
-    Invocation parentInvocation,
-  ) : super(
-          parent,
-          parentInvocation,
-        );
-}
-
-class _FakeLogger_1 extends _i1.SmartFake implements _i3.Logger {
-  _FakeLogger_1(
-    Object parent,
-    Invocation parentInvocation,
-  ) : super(
-          parent,
-          parentInvocation,
-        );
-}
-
-/// A class which mocks [AssetsState].
-///
-/// See the documentation for Mockito's code generation for more information.
-class MockAssetsState extends _i1.Mock implements _i2.AssetsState {
-  @override
-  List<_i4.Asset> get allAssets => (super.noSuchMethod(
-        Invocation.getter(#allAssets),
-        returnValue: <_i4.Asset>[],
-        returnValueForMissingStub: <_i4.Asset>[],
-      ) as List<_i4.Asset>);
-  @override
-  _i5.Future<_i2.AssetsState> withRenderDataStructure(
-          _i6.AssetGridLayoutParameters? layout) =>
-      (super.noSuchMethod(
-        Invocation.method(
-          #withRenderDataStructure,
-          [layout],
-        ),
-        returnValue: _i5.Future<_i2.AssetsState>.value(_FakeAssetsState_0(
-          this,
-          Invocation.method(
-            #withRenderDataStructure,
-            [layout],
-          ),
-        )),
-        returnValueForMissingStub:
-            _i5.Future<_i2.AssetsState>.value(_FakeAssetsState_0(
-          this,
-          Invocation.method(
-            #withRenderDataStructure,
-            [layout],
-          ),
-        )),
-      ) as _i5.Future<_i2.AssetsState>);
-  @override
-  _i2.AssetsState withAdditionalAssets(List<_i4.Asset>? toAdd) =>
-      (super.noSuchMethod(
-        Invocation.method(
-          #withAdditionalAssets,
-          [toAdd],
-        ),
-        returnValue: _FakeAssetsState_0(
-          this,
-          Invocation.method(
-            #withAdditionalAssets,
-            [toAdd],
-          ),
-        ),
-        returnValueForMissingStub: _FakeAssetsState_0(
-          this,
-          Invocation.method(
-            #withAdditionalAssets,
-            [toAdd],
-          ),
-        ),
-      ) as _i2.AssetsState);
-}
-
-/// A class which mocks [AssetNotifier].
-///
-/// See the documentation for Mockito's code generation for more information.
-class MockAssetNotifier extends _i1.Mock implements _i2.AssetNotifier {
-  @override
-  _i3.Logger get log => (super.noSuchMethod(
-        Invocation.getter(#log),
-        returnValue: _FakeLogger_1(
-          this,
-          Invocation.getter(#log),
-        ),
-        returnValueForMissingStub: _FakeLogger_1(
-          this,
-          Invocation.getter(#log),
-        ),
-      ) as _i3.Logger);
-  @override
-  set onError(_i7.ErrorListener? _onError) => super.noSuchMethod(
-        Invocation.setter(
-          #onError,
-          _onError,
-        ),
-        returnValueForMissingStub: null,
-      );
-  @override
-  bool get mounted => (super.noSuchMethod(
-        Invocation.getter(#mounted),
-        returnValue: false,
-        returnValueForMissingStub: false,
-      ) as bool);
-  @override
-  _i5.Stream<_i2.AssetsState> get stream => (super.noSuchMethod(
-        Invocation.getter(#stream),
-        returnValue: _i5.Stream<_i2.AssetsState>.empty(),
-        returnValueForMissingStub: _i5.Stream<_i2.AssetsState>.empty(),
-      ) as _i5.Stream<_i2.AssetsState>);
-  @override
-  _i2.AssetsState get state => (super.noSuchMethod(
-        Invocation.getter(#state),
-        returnValue: _FakeAssetsState_0(
-          this,
-          Invocation.getter(#state),
-        ),
-        returnValueForMissingStub: _FakeAssetsState_0(
-          this,
-          Invocation.getter(#state),
-        ),
-      ) as _i2.AssetsState);
-  @override
-  set state(_i2.AssetsState? value) => super.noSuchMethod(
-        Invocation.setter(
-          #state,
-          value,
-        ),
-        returnValueForMissingStub: null,
-      );
-  @override
-  _i2.AssetsState get debugState => (super.noSuchMethod(
-        Invocation.getter(#debugState),
-        returnValue: _FakeAssetsState_0(
-          this,
-          Invocation.getter(#debugState),
-        ),
-        returnValueForMissingStub: _FakeAssetsState_0(
-          this,
-          Invocation.getter(#debugState),
-        ),
-      ) as _i2.AssetsState);
-  @override
-  bool get hasListeners => (super.noSuchMethod(
-        Invocation.getter(#hasListeners),
-        returnValue: false,
-        returnValueForMissingStub: false,
-      ) as bool);
-  @override
-  _i5.Future<void> rebuildAssetGridDataStructure() => (super.noSuchMethod(
-        Invocation.method(
-          #rebuildAssetGridDataStructure,
-          [],
-        ),
-        returnValue: _i5.Future<void>.value(),
-        returnValueForMissingStub: _i5.Future<void>.value(),
-      ) as _i5.Future<void>);
-  @override
-  _i5.Future<void> getAllAsset({bool? clear = false}) => (super.noSuchMethod(
-        Invocation.method(
-          #getAllAsset,
-          [],
-          {#clear: clear},
-        ),
-        returnValue: _i5.Future<void>.value(),
-        returnValueForMissingStub: _i5.Future<void>.value(),
-      ) as _i5.Future<void>);
-  @override
-  _i5.Future<void> clearAllAsset() => (super.noSuchMethod(
-        Invocation.method(
-          #clearAllAsset,
-          [],
-        ),
-        returnValue: _i5.Future<void>.value(),
-        returnValueForMissingStub: _i5.Future<void>.value(),
-      ) as _i5.Future<void>);
-  @override
-  _i5.Future<void> onNewAssetUploaded(_i4.Asset? newAsset) =>
-      (super.noSuchMethod(
-        Invocation.method(
-          #onNewAssetUploaded,
-          [newAsset],
-        ),
-        returnValue: _i5.Future<void>.value(),
-        returnValueForMissingStub: _i5.Future<void>.value(),
-      ) as _i5.Future<void>);
-  @override
-  _i5.Future<void> deleteAssets(Set<_i4.Asset>? deleteAssets) =>
-      (super.noSuchMethod(
-        Invocation.method(
-          #deleteAssets,
-          [deleteAssets],
-        ),
-        returnValue: _i5.Future<void>.value(),
-        returnValueForMissingStub: _i5.Future<void>.value(),
-      ) as _i5.Future<void>);
-  @override
-  _i5.Future<bool> toggleFavorite(
-    _i4.Asset? asset,
-    bool? status,
-  ) =>
-      (super.noSuchMethod(
-        Invocation.method(
-          #toggleFavorite,
-          [
-            asset,
-            status,
-          ],
-        ),
-        returnValue: _i5.Future<bool>.value(false),
-        returnValueForMissingStub: _i5.Future<bool>.value(false),
-      ) as _i5.Future<bool>);
-  @override
-  _i5.Future<void> toggleArchive(
-    Iterable<_i4.Asset>? assets,
-    bool? status,
-  ) =>
-      (super.noSuchMethod(
-        Invocation.method(
-          #toggleArchive,
-          [
-            assets,
-            status,
-          ],
-        ),
-        returnValue: _i5.Future<void>.value(),
-        returnValueForMissingStub: _i5.Future<void>.value(),
-      ) as _i5.Future<void>);
-  @override
-  bool updateShouldNotify(
-    _i2.AssetsState? old,
-    _i2.AssetsState? current,
-  ) =>
-      (super.noSuchMethod(
-        Invocation.method(
-          #updateShouldNotify,
-          [
-            old,
-            current,
-          ],
-        ),
-        returnValue: false,
-        returnValueForMissingStub: false,
-      ) as bool);
-  @override
-  _i7.RemoveListener addListener(
-    _i8.Listener<_i2.AssetsState>? listener, {
-    bool? fireImmediately = true,
-  }) =>
-      (super.noSuchMethod(
-        Invocation.method(
-          #addListener,
-          [listener],
-          {#fireImmediately: fireImmediately},
-        ),
-        returnValue: () {},
-        returnValueForMissingStub: () {},
-      ) as _i7.RemoveListener);
-  @override
-  void dispose() => super.noSuchMethod(
-        Invocation.method(
-          #dispose,
-          [],
-        ),
-        returnValueForMissingStub: null,
-      );
-}