feat(mobile): Various minor performance improvements (#1176)

* Improve scroll performance by introducing repaint boundaries and moving more calculations to providers.

* Add error handing for malformed dates.

* Remove unused method

* Use compute in different places to improve app performance during heavy tasks

* Fix test

* Refactor `List<RenderAssetGridElement>` to separate `RenderList` class and make `fromAssetGroups` a static method of this class.

* Fix loading indicator bug

* Use provider directly

* `RenderList` refactoring

* `AssetNotifier` refactoring

* Move `combine` to static private method

* Extract compute methods in cache services to static private methods.

* Use `tryParse` instead of `parse` with try/catch for dates.

* Fix bug in caching mechanism.

* Fixed state not being used to trigger conditional rendering

* styling

* Corrected state

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
Matthias Rupp 2023-01-18 16:59:23 +01:00 committed by GitHub
parent 92972ac776
commit 7a1ae8691e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 312 additions and 242 deletions

View file

@ -1,14 +0,0 @@
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';
final renderListProvider = StateProvider((ref) {
var assetGroups = ref.watch(assetGroupByDateTimeProvider);
var settings = ref.watch(appSettingsServiceProvider);
final assetsPerRow = settings.getSetting(AppSettingsEnum.tilesPerRow);
return assetGroupsToRenderList(assetGroups, assetsPerRow);
});

View file

@ -7,9 +7,18 @@ import 'package:immich_mobile/shared/services/json_cache.dart';
class AssetCacheService extends JsonCache<List<Asset>> {
AssetCacheService() : super("asset_cache");
static Future<List<Map<String, dynamic>>> _computeSerialize(
List<Asset> assets) async {
return assets.map((e) => e.toJson()).toList();
}
@override
void put(List<Asset> data) {
putRawData(data.map((e) => e.toJson()).toList());
void put(List<Asset> data) async {
putRawData(await compute(_computeSerialize, data));
}
static Future<List<Asset>> _computeEncode(List<dynamic> data) async {
return data.map((e) => Asset.fromJson(e)).whereNotNull().toList();
}
@override
@ -17,8 +26,7 @@ class AssetCacheService extends JsonCache<List<Asset>> {
try {
final mapList = await readRawData() as List<dynamic>;
final responseData =
mapList.map((e) => Asset.fromJson(e)).whereNotNull().toList();
final responseData = await compute(_computeEncode, mapList);
return responseData;
} catch (e) {

View file

@ -1,5 +1,7 @@
import 'dart:math';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:logging/logging.dart';
@ -33,85 +35,122 @@ class RenderAssetGridElement {
});
}
List<RenderAssetGridElement> assetsToRenderList(
List<Asset> assets,
int assetsPerRow,
) {
List<RenderAssetGridElement> elements = [];
class _AssetGroupsToRenderListComputeParameters {
final String monthFormat;
final String dayFormat;
final String dayFormatYear;
final Map<String, List<Asset>> groups;
final int perRow;
int cursor = 0;
while (cursor < assets.length) {
int rowElements = min(assets.length - cursor, assetsPerRow);
final date = assets[cursor].createdAt;
final rowElement = RenderAssetGridElement(
RenderAssetGridElementType.assetRow,
date: date,
assetRow: RenderAssetGridRow(
assets.sublist(cursor, cursor + rowElements),
),
);
elements.add(rowElement);
cursor += rowElements;
}
return elements;
_AssetGroupsToRenderListComputeParameters(this.monthFormat, this.dayFormat,
this.dayFormatYear, this.groups, this.perRow);
}
List<RenderAssetGridElement> assetGroupsToRenderList(
Map<String, List<Asset>> assetGroups,
int assetsPerRow,
) {
List<RenderAssetGridElement> elements = [];
DateTime? lastDate;
class RenderList {
final List<RenderAssetGridElement> elements;
assetGroups.forEach((groupName, assets) {
try {
final date = DateTime.parse(groupName);
RenderList(this.elements);
static Future<RenderList> _processAssetGroupData(
_AssetGroupsToRenderListComputeParameters data) async {
final monthFormat = DateFormat(data.monthFormat);
final dayFormatSameYear = DateFormat(data.dayFormat);
final dayFormatOtherYear = DateFormat(data.dayFormatYear);
final groups = data.groups;
final perRow = data.perRow;
List<RenderAssetGridElement> elements = [];
DateTime? lastDate;
groups.forEach((groupName, assets) {
try {
final date = DateTime.parse(groupName);
if (lastDate == null || lastDate!.month != date.month) {
// Month title
var monthTitleText = groupName;
var groupDate = DateTime.tryParse(groupName);
if (groupDate != null) {
monthTitleText = monthFormat.format(groupDate);
} else {
log.severe("Failed to format date for day title: $groupName");
}
elements.add(
RenderAssetGridElement(
RenderAssetGridElementType.monthTitle,
title: monthTitleText,
date: date,
),
);
}
// Add group title
var currentYear = DateTime.now().year;
var groupYear = DateTime.parse(groupName).year;
var formatDate =
currentYear == groupYear ? dayFormatSameYear : dayFormatOtherYear;
var dateText = groupName;
var groupDate = DateTime.tryParse(groupName);
if (groupDate != null) {
dateText = formatDate.format(groupDate);
} else {
log.severe("Failed to format date for day title: $groupName");
}
if (lastDate == null || lastDate!.month != date.month) {
elements.add(
RenderAssetGridElement(
RenderAssetGridElementType.monthTitle,
title: groupName,
RenderAssetGridElementType.dayTitle,
title: dateText,
date: date,
),
);
}
// Add group title
elements.add(
RenderAssetGridElement(
RenderAssetGridElementType.dayTitle,
title: groupName,
date: date,
relatedAssetList: assets,
),
);
// Add rows
int cursor = 0;
while (cursor < assets.length) {
int rowElements = min(assets.length - cursor, assetsPerRow);
final rowElement = RenderAssetGridElement(
RenderAssetGridElementType.assetRow,
date: date,
assetRow: RenderAssetGridRow(
assets.sublist(cursor, cursor + rowElements),
relatedAssetList: assets,
),
);
elements.add(rowElement);
cursor += rowElements;
// Add rows
int cursor = 0;
while (cursor < assets.length) {
int rowElements = min(assets.length - cursor, perRow);
final rowElement = RenderAssetGridElement(
RenderAssetGridElementType.assetRow,
date: date,
assetRow: RenderAssetGridRow(
assets.sublist(cursor, cursor + rowElements),
),
);
elements.add(rowElement);
cursor += rowElements;
}
lastDate = date;
} catch (e, stackTrace) {
log.severe(e, stackTrace);
}
});
lastDate = date;
} catch (e, stackTrace) {
log.severe(e, stackTrace);
}
});
return RenderList(elements);
}
return elements;
static Future<RenderList> fromAssetGroups(
Map<String, List<Asset>> assetGroups,
int assetsPerRow,
) async {
// Compute only allows for one parameter. Therefore we pass all parameters in a map
return compute(
_processAssetGroupData,
_AssetGroupsToRenderListComputeParameters(
"monthly_title_text_date_format".tr(),
"daily_title_text_date".tr(),
"daily_title_text_date_year".tr(),
assetGroups,
assetsPerRow,
),
);
}
}

View file

@ -5,14 +5,14 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
class DailyTitleText extends ConsumerWidget {
const DailyTitleText({
Key? key,
required this.isoDate,
required this.text,
required this.multiselectEnabled,
required this.onSelect,
required this.onDeselect,
required this.selected,
}) : super(key: key);
final String isoDate;
final String text;
final bool multiselectEnabled;
final Function onSelect;
final Function onDeselect;
@ -20,13 +20,7 @@ class DailyTitleText extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
var currentYear = DateTime.now().year;
var groupYear = DateTime.parse(isoDate).year;
var formatDateTemplate = currentYear == groupYear
? "daily_title_text_date".tr()
: "daily_title_text_date_year".tr();
var dateText =
DateFormat(formatDateTemplate).format(DateTime.parse(isoDate));
void handleTitleIconClick() {
if (selected) {
@ -46,7 +40,7 @@ class DailyTitleText extends ConsumerWidget {
child: Row(
children: [
Text(
dateText,
text,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,

View file

@ -24,22 +24,10 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
bool _scrolling = false;
final Set<String> _selectedAssets = HashSet();
List<Asset> get _assets {
return widget.renderList
.map((e) {
if (e.type == RenderAssetGridElementType.assetRow) {
return e.assetRow!.assets;
} else {
return List<Asset>.empty();
}
})
.flattened
.toList();
}
Set<Asset> _getSelectedAssets() {
return _selectedAssets
.map((e) => _assets.firstWhereOrNull((a) => a.id == e))
.map((e) => widget.allAssets.firstWhereOrNull((a) => a.id == e))
.whereNotNull()
.toSet();
}
@ -95,9 +83,9 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
}
return ThumbnailImage(
asset: asset,
assetList: _assets,
assetList: widget.allAssets,
multiselectEnabled: widget.selectionActive,
isSelected: _selectedAssets.contains(asset.id),
isSelected: widget.selectionActive && _selectedAssets.contains(asset.id),
onSelect: () => _selectAssets([asset]),
onDeselect: () => _deselectAssets([asset]),
useGrayBoxPlaceholder: true,
@ -137,7 +125,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
List<Asset> assets,
) {
return DailyTitleText(
isoDate: title,
text: title,
multiselectEnabled: widget.selectionActive,
onSelect: () => _selectAssets(assets),
onDeselect: () => _deselectAssets(assets),
@ -146,14 +134,11 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
}
Widget _buildMonthTitle(BuildContext context, String title) {
var monthTitleText = DateFormat("monthly_title_text_date_format".tr())
.format(DateTime.parse(title));
return Padding(
key: Key("month-$title"),
padding: const EdgeInsets.only(left: 12.0, top: 32),
child: Text(
monthTitleText,
title,
style: TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
@ -164,7 +149,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
}
Widget _itemBuilder(BuildContext c, int position) {
final item = widget.renderList[position];
final item = widget.renderList.elements[position];
if (item.type == RenderAssetGridElementType.dayTitle) {
return _buildTitle(c, item.title!, item.relatedAssetList!);
@ -178,7 +163,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
}
Text _labelBuilder(int pos) {
final date = widget.renderList[pos].date;
final date = widget.renderList.elements[pos].date;
return Text(
DateFormat.yMMMd().format(date),
style: const TextStyle(
@ -196,7 +181,7 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
}
Widget _buildAssetGrid() {
final useDragScrolling = _assets.length >= 20;
final useDragScrolling = widget.allAssets.length >= 20;
void dragScrolling(bool active) {
setState(() {
@ -208,7 +193,8 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
itemBuilder: _itemBuilder,
itemPositionsListener: _itemPositionsListener,
itemScrollController: _itemScrollController,
itemCount: widget.renderList.length,
itemCount: widget.renderList.elements.length,
addRepaintBoundaries: true,
);
if (!useDragScrolling) {
@ -250,16 +236,18 @@ class ImmichAssetGridState extends State<ImmichAssetGrid> {
}
class ImmichAssetGrid extends StatefulWidget {
final List<RenderAssetGridElement> renderList;
final RenderList renderList;
final int assetsPerRow;
final double margin;
final bool showStorageIndicator;
final ImmichAssetGridSelectionListener? listener;
final bool selectionActive;
final List<Asset> allAssets;
const ImmichAssetGrid({
super.key,
required this.renderList,
required this.allAssets,
required this.assetsPerRow,
required this.showStorageIndicator,
this.listener,

View file

@ -8,7 +8,6 @@ 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/services/album.service.dart';
import 'package:immich_mobile/modules/home/providers/home_page_render_list_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';
@ -32,7 +31,6 @@ class HomePage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final appSettingService = ref.watch(appSettingsServiceProvider);
var renderList = ref.watch(renderListProvider);
final multiselectEnabled = ref.watch(multiselectProvider.notifier);
final selectionEnabledHook = useState(false);
@ -212,10 +210,12 @@ class HomePage extends HookConsumerWidget {
top: selectionEnabledHook.value ? 0 : 60,
bottom: 0.0,
),
child: ref.watch(assetProvider).isEmpty
child: ref.watch(assetProvider).renderList == null ||
ref.watch(assetProvider).allAssets.isEmpty
? buildLoadingIndicator()
: ImmichAssetGrid(
renderList: renderList,
renderList: ref.watch(assetProvider).renderList!,
allAssets: ref.watch(assetProvider).allAssets,
assetsPerRow: appSettingService
.getSetting(AppSettingsEnum.tilesPerRow),
showStorageIndicator: appSettingService