improved performance and battery usage on older devices

This commit is contained in:
2026-09-25 23:58:08 +02:00
parent ed8e52f475
commit 56a497985b
70 changed files with 1631 additions and 621 deletions
@@ -69,7 +69,7 @@ class LoadableStateConsumer<
// null mid-refetch, and toggling the RefreshIndicator on that signal would
// rebuild the tree under the ListView and reset its scroll position.
final content = SizedBox(
height: MediaQuery.of(context).size.height,
height: MediaQuery.sizeOf(context).height,
child: hasContent
? child(typedData as TState, isLoading)
: const SizedBox.shrink(),
@@ -95,10 +95,12 @@ class _LoadableStateErrorBarTextState extends State<LoadableStateErrorBarText> {
late Timer _rebuildTimer;
@override
void initState() {
_rebuildTimer = Timer.periodic(
const Duration(seconds: 10),
(timer) => setState(() {}),
);
// Only refresh the relative "last updated" text while this page is the
// visible one; offstage tabs and covered routes have tickers disabled.
_rebuildTimer = Timer.periodic(const Duration(seconds: 10), (timer) {
if (!mounted) return;
if (TickerMode.getValuesNotifier(context).value.enabled) setState(() {});
});
super.initState();
}
@@ -33,6 +33,9 @@ class _LoadableStatePrimaryLoadingState
extends State<LoadableStatePrimaryLoading> {
Timer? _slowHintTimer;
bool _showSlowHint = false;
// An indeterminate spinner ticks every frame even at opacity 0, so it is
// unmounted once the fade-out finished instead of just being hidden.
late bool _spinnerMounted = widget.visible;
@override
void initState() {
@@ -44,6 +47,7 @@ class _LoadableStatePrimaryLoadingState
void didUpdateWidget(covariant LoadableStatePrimaryLoading oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.visible != oldWidget.visible) _restartSlowHintTimer();
if (widget.visible) _spinnerMounted = true;
}
void _restartSlowHintTimer() {
@@ -62,43 +66,49 @@ class _LoadableStatePrimaryLoadingState
}
@override
Widget build(BuildContext context) {
Widget build(BuildContext context) => AnimatedOpacity(
opacity: widget.visible ? 1.0 : 0.0,
duration: LoadableStateConsumer.animationDuration,
curve: Curves.easeInOut,
onEnd: () {
if (!widget.visible && _spinnerMounted) {
setState(() => _spinnerMounted = false);
}
},
child: _spinnerMounted ? _spinner(context) : const SizedBox.shrink(),
);
Widget _spinner(BuildContext context) {
final status =
widget.statusText ??
(_showSlowHint ? LoadableStatePrimaryLoading.slowHintText : null);
return AnimatedOpacity(
opacity: widget.visible ? 1.0 : 0.0,
duration: LoadableStateConsumer.animationDuration,
curve: Curves.easeInOut,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const AppProgressIndicator.large(),
AnimatedSwitcher(
duration: LoadableStateConsumer.animationDuration,
child: status == null
? const SizedBox.shrink()
: Padding(
key: ValueKey(status),
padding: const EdgeInsets.only(
top: 16,
left: 24,
right: 24,
),
child: Text(
status,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13,
color: Theme.of(context).hintColor,
),
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const AppProgressIndicator.large(),
AnimatedSwitcher(
duration: LoadableStateConsumer.animationDuration,
child: status == null
? const SizedBox.shrink()
: Padding(
key: ValueKey(status),
padding: const EdgeInsets.only(
top: 16,
left: 24,
right: 24,
),
child: Text(
status,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13,
color: Theme.of(context).hintColor,
),
),
),
],
),
),
),
],
),
);
}