improved performance and battery usage on older devices
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
import 'dart:convert';
|
||||
|
||||
/// Content equality for API models that define no `==` but serialise to JSON.
|
||||
/// Comparing the encoded strings is cheaper than a deep map walk and treats
|
||||
/// both sides identically (same `toJson`, same field order).
|
||||
bool sameJson(Object? a, Object? b) =>
|
||||
a != null && b != null && jsonEncode(a) == jsonEncode(b);
|
||||
@@ -0,0 +1,75 @@
|
||||
import 'package:rrule/rrule.dart';
|
||||
|
||||
/// Memoised RRULE expansion.
|
||||
///
|
||||
/// `RecurrenceRule.getInstances` always starts at the series anchor, so asking
|
||||
/// for one week of a daily event created two years ago walks ~750 occurrences
|
||||
/// (tens of ms each time on older phones) — and the calendar asks once per
|
||||
/// week and event, the home widget twice per publish. Here each series keeps
|
||||
/// its parsed rule, the occurrences found so far and the live iterator, so
|
||||
/// later queries only extend the expansion instead of restarting it.
|
||||
class RecurrenceOccurrences {
|
||||
RecurrenceOccurrences._();
|
||||
|
||||
static const int _maxSeries = 64;
|
||||
static final Map<(String, DateTime), _Series> _cache = {};
|
||||
|
||||
/// UTC occurrences of [rule] anchored at [anchorUtc] in
|
||||
/// `[fromUtc, toUtc)`, in ascending order. Throws like
|
||||
/// [RecurrenceRule.fromString] for an invalid rule.
|
||||
static List<DateTime> between(
|
||||
String rule,
|
||||
DateTime anchorUtc,
|
||||
DateTime fromUtc,
|
||||
DateTime toUtc,
|
||||
) {
|
||||
final key = (rule, anchorUtc);
|
||||
var series = _cache.remove(key);
|
||||
series ??= _Series(RecurrenceRule.fromString(rule), anchorUtc);
|
||||
// Re-insert to keep the map in least-recently-used order.
|
||||
_cache[key] = series;
|
||||
if (_cache.length > _maxSeries) _cache.remove(_cache.keys.first);
|
||||
return series.between(fromUtc, toUtc);
|
||||
}
|
||||
|
||||
/// Drops all memoised series (tests).
|
||||
static void clear() => _cache.clear();
|
||||
}
|
||||
|
||||
class _Series {
|
||||
final Iterator<DateTime> _iterator;
|
||||
final List<DateTime> _found = [];
|
||||
bool _exhausted = false;
|
||||
|
||||
_Series(RecurrenceRule rule, DateTime anchorUtc)
|
||||
: _iterator = rule.getInstances(start: anchorUtc).iterator;
|
||||
|
||||
List<DateTime> between(DateTime fromUtc, DateTime toUtc) {
|
||||
while (!_exhausted && (_found.isEmpty || _found.last.isBefore(toUtc))) {
|
||||
if (_iterator.moveNext()) {
|
||||
_found.add(_iterator.current);
|
||||
} else {
|
||||
_exhausted = true;
|
||||
}
|
||||
}
|
||||
final start = _lowerBound(fromUtc);
|
||||
return [
|
||||
for (var i = start; i < _found.length && _found[i].isBefore(toUtc); i++)
|
||||
_found[i],
|
||||
];
|
||||
}
|
||||
|
||||
int _lowerBound(DateTime value) {
|
||||
var low = 0;
|
||||
var high = _found.length;
|
||||
while (low < high) {
|
||||
final mid = (low + high) >> 1;
|
||||
if (_found[mid].isBefore(value)) {
|
||||
low = mid + 1;
|
||||
} else {
|
||||
high = mid;
|
||||
}
|
||||
}
|
||||
return low;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Wraps [provider] so it decodes at most [scale] × the screen's longest
|
||||
/// physical side (capped at [maxPx]). Camera-sized photos otherwise decode at
|
||||
/// full resolution — 50–200 MB each — which gets the app killed on
|
||||
/// low-memory devices.
|
||||
ImageProvider screenBoundImage(
|
||||
BuildContext context,
|
||||
ImageProvider provider, {
|
||||
double scale = 1,
|
||||
int maxPx = 4096,
|
||||
}) {
|
||||
final bound =
|
||||
(MediaQuery.sizeOf(context).longestSide *
|
||||
MediaQuery.devicePixelRatioOf(context) *
|
||||
scale)
|
||||
.round()
|
||||
.clamp(1, maxPx);
|
||||
return ResizeImage(
|
||||
provider,
|
||||
width: bound,
|
||||
height: bound,
|
||||
policy: ResizeImagePolicy.fit,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import '../model/account_data.dart';
|
||||
|
||||
/// Joins concurrent calls into the one already running for the same account
|
||||
/// session. A run left over from a signed-out session never blocks the next
|
||||
/// account's first call.
|
||||
class SessionSingleFlight {
|
||||
Future<void>? _running;
|
||||
int? _epoch;
|
||||
|
||||
Future<void> run(Future<void> Function() action) {
|
||||
final epoch = AccountData().sessionEpoch;
|
||||
final running = _running;
|
||||
if (running != null && _epoch == epoch) return running;
|
||||
_epoch = epoch;
|
||||
late final Future<void> current;
|
||||
current = action().whenComplete(() {
|
||||
if (identical(_running, current)) _running = null;
|
||||
});
|
||||
return _running = current;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user