use keyed state hook

This commit is contained in:
Fabian Freund
2026-07-02 11:10:05 +02:00
parent 867fd47a8f
commit 4c33183c97
7 changed files with 83 additions and 46 deletions
@@ -72,6 +72,7 @@ import 'package:weblibre/features/user/data/models/general_settings.dart';
import 'package:weblibre/features/user/data/models/proxy_routing_settings.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
import 'package:weblibre/features/user/domain/repositories/proxy_routing_settings.dart';
import 'package:weblibre/presentation/hooks/keyed_state.dart';
import 'package:weblibre/utils/move_to_background.dart';
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
@@ -687,15 +688,11 @@ class BrowserScreen extends HookConsumerWidget {
// Track dismissed state without triggering full rebuild on every
// hide/show. Updated via ref.listen below and synced on tab switch.
final toolbarDismissed = useState(false);
// Sync dismissed state when selected tab changes
useEffect(() {
toolbarDismissed.value =
ref.read(toolbarVisibilityControllerProvider(selectedTabId)) ==
ToolbarVisibility.dismissed;
return null;
}, [selectedTabId]);
final toolbarDismissed = useKeyedState(
ref.read(toolbarVisibilityControllerProvider(selectedTabId)) ==
ToolbarVisibility.dismissed,
[selectedTabId],
);
final stableToolbarHeight =
autoHideTabBar && !toolbarDismissed.value && !tabInFullScreen
@@ -32,6 +32,7 @@ import 'package:weblibre/features/geckoview/features/top_sites/domain/entities/t
import 'package:weblibre/features/geckoview/features/top_sites/domain/entities/top_site_source.dart';
import 'package:weblibre/features/geckoview/features/top_sites/domain/providers.dart';
import 'package:weblibre/features/geckoview/features/top_sites/domain/repositories/top_site_repository.dart';
import 'package:weblibre/presentation/hooks/keyed_state.dart';
import 'package:weblibre/presentation/widgets/url_icon.dart';
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
@@ -228,12 +229,7 @@ class _ReorderableTopSitesGrid extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final localItems = useState(persistedItems);
useEffect(() {
localItems.value = persistedItems;
return null;
}, [persistedItems]);
final localItems = useKeyedState(persistedItems, [persistedItems]);
return SliverToBoxAdapter(
child: Padding(
@@ -28,6 +28,7 @@ import 'package:weblibre/features/settings/presentation/widgets/settings_detail.
import 'package:weblibre/features/user/data/models/general_settings.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
import 'package:weblibre/presentation/hooks/cached_future.dart';
import 'package:weblibre/presentation/hooks/keyed_state.dart';
const List<SettingsSectionDefinition> generalSettingsSections = [
SettingsSectionDefinition(
@@ -198,12 +199,7 @@ class _UiZoomSection extends HookConsumerWidget {
final uiScaleFactor = ref.watch(
generalSettingsWithDefaultsProvider.select((s) => s.uiScaleFactor),
);
final sliderValue = useState(uiScaleFactor);
useEffect(() {
sliderValue.value = uiScaleFactor;
return null;
}, [uiScaleFactor]);
final sliderValue = useKeyedState(uiScaleFactor, [uiScaleFactor]);
final sliderLabel = '${(sliderValue.value * 100).round()}%';
@@ -18,7 +18,6 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
@@ -27,6 +26,7 @@ import 'package:weblibre/features/user/data/models/engine_settings.dart';
import 'package:weblibre/features/user/data/models/general_settings.dart';
import 'package:weblibre/features/user/domain/repositories/engine_settings.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
import 'package:weblibre/presentation/hooks/keyed_state.dart';
const List<SettingsSectionDefinition> webContentSettingsSections = [
SettingsSectionDefinition(
@@ -178,12 +178,7 @@ class _FontSizeFactorSlider extends HookConsumerWidget {
final fontSizeFactor = ref.watch(
engineSettingsWithDefaultsProvider.select((s) => s.fontSizeFactor),
);
final sliderValue = useState(fontSizeFactor);
useEffect(() {
sliderValue.value = fontSizeFactor;
return null;
}, [fontSizeFactor]);
final sliderValue = useKeyedState(fontSizeFactor, [fontSizeFactor]);
final sliderLabel = '${(sliderValue.value * 100).round()}%';
@@ -18,7 +18,6 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:weblibre/core/routing/routes.dart';
@@ -26,6 +25,7 @@ import 'package:weblibre/features/settings/presentation/controllers/save_setting
import 'package:weblibre/features/settings/presentation/widgets/settings_detail.dart';
import 'package:weblibre/features/user/data/models/general_settings.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
import 'package:weblibre/presentation/hooks/keyed_state.dart';
const List<SettingsSectionDefinition> toolbarLayoutSettingsSections = [
SettingsSectionDefinition(
@@ -460,11 +460,7 @@ class _QuickTabSwitcherTitleWidthTile extends HookConsumerWidget {
),
);
final sliderValue = useState(titleWidth);
useEffect(() {
sliderValue.value = titleWidth;
return null;
}, [titleWidth]);
final sliderValue = useKeyedState(titleWidth, [titleWidth]);
final enabled = switcherEnabled && showTitles;
@@ -626,11 +622,9 @@ class _QuickTabSwitcherHierarchyGlyphsTile extends HookConsumerWidget {
),
);
final sliderValue = useState(hierarchyGlyphs.toDouble());
useEffect(() {
sliderValue.value = hierarchyGlyphs.toDouble();
return null;
}, [hierarchyGlyphs]);
final sliderValue = useKeyedState(hierarchyGlyphs.toDouble(), [
hierarchyGlyphs,
]);
final currentGlyphs = sliderValue.value.round();
final enabled = switcherEnabled;
@@ -7,6 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:search_protocol/search_protocol.dart';
import 'package:weblibre/core/providers/persisted_bool.dart';
import 'package:weblibre/features/web_search/domain/controllers/search_controller.dart';
import 'package:weblibre/presentation/hooks/keyed_state.dart';
import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
import 'package:weblibre/presentation/widgets/url_icon.dart';
@@ -541,16 +542,11 @@ class WebSearchInfoboxCarousel extends HookConsumerWidget {
// Natural (content) height of each page, measured off-stage. `null` until
// a page has been laid out at least once.
final heights = useState<List<double?>>(
final heights = useKeyedState<List<double?>>(
List<double?>.filled(infos.length, null),
[infos.length],
);
// Reset the measurement cache when the page set changes.
useEffect(() {
heights.value = List<double?>.filled(infos.length, null);
return null;
}, [infos.length]);
useEffect(() {
void listener() {
final page = controller.page?.round() ?? 0;
@@ -0,0 +1,63 @@
/*
* Copyright (c) 2024-2026 Fabian Freund.
*
* This file is part of WebLibre
* (see https://weblibre.eu).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:flutter/widgets.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
/// Creates a [ValueNotifier] that is re-created when [keys] change.
ValueNotifier<T> useKeyedState<T>(T initialData, List<Object?> keys) {
return use(_KeyedStateHook<T>(initialData: initialData, keys: keys));
}
class _KeyedStateHook<T> extends Hook<ValueNotifier<T>> {
const _KeyedStateHook({
required this.initialData,
required List<Object?> keys,
}) : super(keys: keys);
final T initialData;
@override
_KeyedStateHookState<T> createState() => _KeyedStateHookState<T>();
}
class _KeyedStateHookState<T>
extends HookState<ValueNotifier<T>, _KeyedStateHook<T>> {
late final _state = ValueNotifier<T>(hook.initialData)
..addListener(_listener);
@override
void dispose() {
_state.dispose();
super.dispose();
}
@override
ValueNotifier<T> build(BuildContext context) => _state;
void _listener() {
setState(() {});
}
@override
Object? get debugValue => _state.value;
@override
String get debugLabel => 'useKeyedState<$T>';
}