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,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,