On a 32-bit Amlogic TV box, 57% of UI frames while moving focus along a rail missed the 16.68 ms budget, and the median frame was already over it at 20.7 ms. The GPU was idle throughout (raster p90 9.1 ms, zero frames over budget) -- all of it was main-isolate work, split roughly evenly between layout and semantics. Five separate causes, all measured: - The horizontal rail used `itemExtentBuilder`, so `RenderSliverVariedExtentList` walked every preceding index on each realized child, in every layout pass, and re-resolved the trailing slot inside the closure. Pinning the trailing cell to the card extent makes the list uniform, which restores O(1) offset, index and max-extent math. - The sidebar-expand tween wrapped the whole shell with a `LayoutBuilder` inside its builder, so ~15 ticks per focus flip rebuilt `SideNavigationRail` -- and its non-virtualized child list -- during the layout phase. The tween now wraps only the content `Positioned`, matching `SideNavigationBleedBuilder`. - Each nav item crossfaded through two `Opacity` subtrees, i.e. 14-18 offscreen save layers per frame for the whole 250 ms morph. It now fades colour alpha on the leaf, the same substitution `AnimatedDimScrim` already documents. - `HorizontalScrollWithArrows` kept a scroll listener on platforms where its arrow chrome is compiled out, so crossing either scroll boundary `setState`d a whole rail row of cards. Two full-row rebuild storms per traversal, for arrows that cannot appear. - The rail's semantic proxy rebuilt its label and six closures on every `_RailFocusModel` notification, including vertical-scroll flips. Also: one merged listener per `SettingsBuilder` instead of six, and the media card semantic label is cached rather than rebuilt and then discarded by the rail's `ExcludeSemantics`. Measured on the device, median of three runs: frames over budget 120/212 -> 32/246, total UI-thread work 4067 ms -> 1845 ms, layout p90 18.2 -> 8.1 ms, build p90 7.95 -> 2.42 ms. The sidebar scenario went from 121/148 frames over budget to 32/184.
68 lines
2.5 KiB
Dart
68 lines
2.5 KiB
Dart
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/widgets.dart';
|
|
|
|
import '../services/settings_service.dart';
|
|
|
|
/// Non-reactive one-shot read of [pref] from the singleton [SettingsService].
|
|
/// Use for callbacks and event handlers that need the current value but don't
|
|
/// need to rebuild on change. For reactive reads in build methods, prefer
|
|
/// [SettingValueBuilder] / [SettingsBuilder] so only the dependent subtree rebuilds.
|
|
extension SettingsContextRead on BuildContext {
|
|
T settingsRead<T>(Pref<T> pref) => SettingsService.instance.read(pref);
|
|
}
|
|
|
|
/// Rebuild [builder] when any of [prefs] changes. Use when a widget's output
|
|
/// depends on multiple settings (conditional visibility, derived values).
|
|
/// Inside [builder], read with [SettingsService.read] directly — the rebuild
|
|
/// is already wired through.
|
|
///
|
|
/// Stateful so widget updates can switch preference groups without rebuilding
|
|
/// subscriptions in [build]. Const preference lists share the service's
|
|
/// identity-cached fan-out; dynamic lists keep this state's existing lifecycle.
|
|
class SettingsBuilder extends StatefulWidget {
|
|
final List<Pref<Object?>> prefs;
|
|
final WidgetBuilder builder;
|
|
|
|
const SettingsBuilder({super.key, required this.prefs, required this.builder});
|
|
|
|
@override
|
|
State<SettingsBuilder> createState() => _SettingsBuilderState();
|
|
}
|
|
|
|
class _SettingsBuilderState extends State<SettingsBuilder> {
|
|
late Listenable _merged = _merge();
|
|
|
|
Listenable _merge() => SettingsService.instance.listenableOfAll(widget.prefs);
|
|
|
|
@override
|
|
void didUpdateWidget(covariant SettingsBuilder oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (!listEquals(widget.prefs, oldWidget.prefs)) _merged = _merge();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ListenableBuilder(listenable: _merged, builder: (context, _) => widget.builder(context));
|
|
}
|
|
}
|
|
|
|
/// Single-pref wrapper around [ValueListenableBuilder] keyed by a [Pref].
|
|
/// Equivalent to `ValueListenableBuilder(valueListenable: svc.listenable(pref))`
|
|
/// but reads cleaner at call sites that don't already hold the service.
|
|
class SettingValueBuilder<T> extends StatelessWidget {
|
|
final Pref<T> pref;
|
|
final Widget Function(BuildContext, T value, Widget? child) builder;
|
|
final Widget? child;
|
|
|
|
const SettingValueBuilder({super.key, required this.pref, required this.builder, this.child});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ValueListenableBuilder<T>(
|
|
valueListenable: SettingsService.instance.listenable(pref),
|
|
builder: builder,
|
|
child: child,
|
|
);
|
|
}
|
|
}
|