From 59c567d49572d3a48a952c1abad914635d40d19c Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Fri, 3 Oct 2025 07:45:01 +0200 Subject: [PATCH] fix riverpod lifecycle issues --- .../domain/controllers/bottom_sheet.dart | 2 +- .../domain/controllers/bottom_sheet.g.dart | 4 +- .../geckoview/domain/controllers/overlay.dart | 2 +- .../domain/controllers/overlay.g.dart | 4 +- .../browser/presentation/screens/browser.dart | 585 +++++++++--------- 5 files changed, 304 insertions(+), 293 deletions(-) diff --git a/app/lib/features/geckoview/domain/controllers/bottom_sheet.dart b/app/lib/features/geckoview/domain/controllers/bottom_sheet.dart index af50b390..13806c26 100644 --- a/app/lib/features/geckoview/domain/controllers/bottom_sheet.dart +++ b/app/lib/features/geckoview/domain/controllers/bottom_sheet.dart @@ -23,7 +23,7 @@ import 'package:weblibre/features/geckoview/features/browser/domain/entities/she part 'bottom_sheet.g.dart'; -@Riverpod() +@Riverpod(keepAlive: true) class BottomSheetController extends _$BottomSheetController { @override Sheet? build() { diff --git a/app/lib/features/geckoview/domain/controllers/bottom_sheet.g.dart b/app/lib/features/geckoview/domain/controllers/bottom_sheet.g.dart index 0c1999de..b96f607e 100644 --- a/app/lib/features/geckoview/domain/controllers/bottom_sheet.g.dart +++ b/app/lib/features/geckoview/domain/controllers/bottom_sheet.g.dart @@ -20,7 +20,7 @@ final class BottomSheetControllerProvider argument: null, retry: null, name: r'bottomSheetControllerProvider', - isAutoDispose: true, + isAutoDispose: false, dependencies: null, $allTransitiveDependencies: null, ); @@ -42,7 +42,7 @@ final class BottomSheetControllerProvider } String _$bottomSheetControllerHash() => - r'0b0ea53a96b80b8c1cba79b1d5913274b7127d5b'; + r'94b9467731de45d4edd24028d76279e17fc26d02'; abstract class _$BottomSheetController extends $Notifier { Sheet? build(); diff --git a/app/lib/features/geckoview/domain/controllers/overlay.dart b/app/lib/features/geckoview/domain/controllers/overlay.dart index 7423256d..11bd1eff 100644 --- a/app/lib/features/geckoview/domain/controllers/overlay.dart +++ b/app/lib/features/geckoview/domain/controllers/overlay.dart @@ -22,7 +22,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'overlay.g.dart'; -@Riverpod() +@Riverpod(keepAlive: true) class OverlayController extends _$OverlayController { @override WidgetBuilder? build() { diff --git a/app/lib/features/geckoview/domain/controllers/overlay.g.dart b/app/lib/features/geckoview/domain/controllers/overlay.g.dart index f993203d..64d0965b 100644 --- a/app/lib/features/geckoview/domain/controllers/overlay.g.dart +++ b/app/lib/features/geckoview/domain/controllers/overlay.g.dart @@ -20,7 +20,7 @@ final class OverlayControllerProvider argument: null, retry: null, name: r'overlayControllerProvider', - isAutoDispose: true, + isAutoDispose: false, dependencies: null, $allTransitiveDependencies: null, ); @@ -41,7 +41,7 @@ final class OverlayControllerProvider } } -String _$overlayControllerHash() => r'7b7885e07ae557c14199b6942b5d6392592ab747'; +String _$overlayControllerHash() => r'd0cd7c4cf867397f10f801ec2f7733be56520fb3'; abstract class _$OverlayController extends $Notifier { WidgetBuilder? build(); diff --git a/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart b/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart index 8a24be2b..d2870a97 100644 --- a/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart +++ b/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart @@ -107,331 +107,342 @@ class BrowserScreen extends HookConsumerWidget { final sheetController = useRef(null); - return PopScope( - //We need this for BackButtonListener to work downstream - //No direct pop result will be handled here - canPop: false, - child: Theme( - data: themeData, - child: Scaffold( - extendBodyBehindAppBar: tabInFullScreen, - bottomNavigationBar: HookConsumer( - builder: (context, ref, child) { - final tabId = ref.watch(selectedTabProvider); - final displayedSheet = ref.watch(bottomSheetControllerProvider); + return TickerMode( + //Lets not pause riverpod subscriptions for nnow + enabled: true, + child: PopScope( + //We need this for BackButtonListener to work downstream + //No direct pop result will be handled here + canPop: false, + child: Theme( + data: themeData, + child: Scaffold( + extendBodyBehindAppBar: tabInFullScreen, + bottomNavigationBar: HookConsumer( + builder: (context, ref, child) { + final tabId = ref.watch(selectedTabProvider); + final displayedSheet = ref.watch(bottomSheetControllerProvider); - final tabInFullScreen = ref.watch( - selectedTabStateProvider.select( - (value) => value?.isFullScreen ?? false, - ), - ); - - final autoHideTabBar = ref.watch( - generalSettingsWithDefaultsProvider.select( - (value) => value.autoHideTabBar, - ), - ); - - if (!autoHideTabBar) { - return Visibility( - visible: !tabInFullScreen, - child: BrowserBottomAppBar(displayedSheet: displayedSheet), + final tabInFullScreen = ref.watch( + selectedTabStateProvider.select( + (value) => value?.isFullScreen ?? false, + ), ); - } - final appBarVisible = useValueListenable(showAppBar); - final diffAcc = useRef(0.0); + final autoHideTabBar = ref.watch( + generalSettingsWithDefaultsProvider.select( + (value) => value.autoHideTabBar, + ), + ); - void resetHiddenState() { - showAppBar.value = true; - diffAcc.value = 0.0; - } - - useEffect(() { - WidgetsBinding.instance.addPostFrameCallback((_) { - resetHiddenState(); - }); - - return null; - }, [tabId]); - - useOnAppLifecycleStateChange((previous, current) { - if (current == AppLifecycleState.resumed) { - resetHiddenState(); + if (!autoHideTabBar) { + return Visibility( + visible: !tabInFullScreen, + child: BrowserBottomAppBar(displayedSheet: displayedSheet), + ); } - }); - ref.listen( - tabStateProvider(tabId).select((value) => value?.isLoading), - (previous, next) { - if (next == true) { + final appBarVisible = useValueListenable(showAppBar); + final diffAcc = useRef(0.0); + + void resetHiddenState() { + showAppBar.value = true; + diffAcc.value = 0.0; + } + + useEffect(() { + WidgetsBinding.instance.addPostFrameCallback((_) { + resetHiddenState(); + }); + + return null; + }, [tabId]); + + useOnAppLifecycleStateChange((previous, current) { + if (current == AppLifecycleState.resumed) { resetHiddenState(); } - }, - ); + }); - ref.listen( - tabStateProvider(tabId).select((value) => value?.historyState), - (previous, next) { - if (next != null && previous != null) { - if (previous != next) { + ref.listen( + tabStateProvider(tabId).select((value) => value?.isLoading), + (previous, next) { + if (next == true) { resetHiddenState(); } - } - }, - ); + }, + ); - ref.listen( - tabScrollYProvider(tabId, const Duration(milliseconds: 50)), - (previous, next) { - if (previous?.value != null && next.value != null) { - final diff = previous!.value! - next.value!; - if (diff < 0) { - if (diffAcc.value > 0) { - diffAcc.value = 0.0; - } - - diffAcc.value += diff; - if (diffAcc.value.abs() > kToolbarHeight * 1.5) { - showAppBar.value = false; - } - } else if (diff > 0) { - if (diffAcc.value < 0) { - diffAcc.value = 0.0; - } - - diffAcc.value += diff; - if (diffAcc.value.abs() > (kToolbarHeight / 2)) { + ref.listen( + tabStateProvider( + tabId, + ).select((value) => value?.historyState), + (previous, next) { + if (next != null && previous != null) { + if (previous != next) { resetHiddenState(); } } + }, + ); + + ref.listen( + tabScrollYProvider(tabId, const Duration(milliseconds: 50)), + (previous, next) { + if (previous?.value != null && next.value != null) { + final diff = previous!.value! - next.value!; + if (diff < 0) { + if (diffAcc.value > 0) { + diffAcc.value = 0.0; + } + + diffAcc.value += diff; + if (diffAcc.value.abs() > kToolbarHeight * 1.5) { + showAppBar.value = false; + } + } else if (diff > 0) { + if (diffAcc.value < 0) { + diffAcc.value = 0.0; + } + + diffAcc.value += diff; + if (diffAcc.value.abs() > (kToolbarHeight / 2)) { + resetHiddenState(); + } + } + } + }, + ); + + return Visibility( + visible: !tabInFullScreen && appBarVisible, + child: BrowserBottomAppBar(displayedSheet: displayedSheet), + ); + }, + ), + body: Consumer( + child: DragTarget( + onMove: (details) { + ref + .read(willAcceptDropProvider.notifier) + .setData(DeleteDropData(details.data.tabId)); + }, + onLeave: (data) { + ref.read(willAcceptDropProvider.notifier).clear(); + }, + onAcceptWithDetails: (details) async { + ref.read(willAcceptDropProvider.notifier).clear(); + await ref + .read(tabRepositoryProvider.notifier) + .closeTab(details.data.tabId); + + if (context.mounted) { + ui_helper.showTabUndoClose( + context, + ref.read(tabRepositoryProvider.notifier).undoClose, + ); } }, - ); + builder: (context, _, _) { + return OverlayPortal( + controller: overlayController, + overlayChildBuilder: (context) { + return overlayBuilder!.call(context); + }, + child: Listener( + onPointerDown: sheetDisplayed + ? (_) { + ref + .read(bottomSheetControllerProvider.notifier) + .dismiss(); + } + : null, + child: BackButtonListener( + onBackButtonPressed: () async { + final tabState = ref.read(selectedTabStateProvider); - return Visibility( - visible: !tabInFullScreen && appBarVisible, - child: BrowserBottomAppBar(displayedSheet: displayedSheet), - ); - }, - ), - body: Consumer( - child: DragTarget( - onMove: (details) { - ref - .read(willAcceptDropProvider.notifier) - .setData(DeleteDropData(details.data.tabId)); - }, - onLeave: (data) { - ref.read(willAcceptDropProvider.notifier).clear(); - }, - onAcceptWithDetails: (details) async { - ref.read(willAcceptDropProvider.notifier).clear(); - await ref - .read(tabRepositoryProvider.notifier) - .closeTab(details.data.tabId); + final tabCount = ref.read( + tabListProvider.select((tabs) => tabs.value.length), + ); - if (context.mounted) { - ui_helper.showTabUndoClose( - context, - ref.read(tabRepositoryProvider.notifier).undoClose, - ); - } - }, - builder: (context, _, _) { - return OverlayPortal( - controller: overlayController, - overlayChildBuilder: (context) { - return overlayBuilder!.call(context); - }, - child: Listener( - onPointerDown: sheetDisplayed - ? (_) { + //Don't do anything if a child route is active + if (GoRouterState.of(context).topRoute?.name != + BrowserRoute.name) { + return false; + } + + if (sheetDisplayed) { ref .read(bottomSheetControllerProvider.notifier) .dismiss(); + return true; } - : null, - child: BackButtonListener( - onBackButtonPressed: () async { - final tabState = ref.read(selectedTabStateProvider); - final tabCount = ref.read( - tabListProvider.select((tabs) => tabs.value.length), - ); + if (overlayBuilder != null) { + ref + .read(overlayControllerProvider.notifier) + .dismiss(); + return true; + } - //Don't do anything if a child route is active - if (GoRouterState.of(context).topRoute?.name != - BrowserRoute.name) { - return false; - } + if (tabState?.isFullScreen == true) { + await ref + .read(selectedTabSessionProvider) + .exitFullscreen(); + return true; + } - if (sheetDisplayed) { + //Make sure app bar is visible + showAppBar.value = true; + + if (tabState?.isLoading == true) { + lastBackButtonPress.value = null; + + final controller = ref.read( + selectedTabSessionProvider, + ); + + await controller.stopLoading(); + return true; + } else if (tabState?.readerableState.active == true) { + lastBackButtonPress.value = null; + + await ref + .read( + readerableScreenControllerProvider.notifier, + ) + .toggleReaderView(false); + + return true; + } else if (tabState?.historyState.canGoBack == true) { + lastBackButtonPress.value = null; + + final controller = ref.read( + selectedTabSessionProvider, + ); + + await controller.goBack(); + return true; + } + + //Go router has routes to go back to + if (context.canPop()) { + return true; + } + + if (ref + .read(tabRepositoryProvider.notifier) + .hasLaunchedFromIntent(tabState?.id)) { + //Mark back as unhandled and navigator will pop + await SystemNavigator.pop(); + return false; + } + + if (lastBackButtonPress.value != null && + DateTime.now().difference( + lastBackButtonPress.value!, + ) < + _backButtonPressTimeout) { + lastBackButtonPress.value = null; + + if (tabState != null && tabCount > 1) { + await ref + .read(tabRepositoryProvider.notifier) + .closeTab(tabState.id); + + if (context.mounted) { + ui_helper.showTabUndoClose( + context, + ref + .read(tabRepositoryProvider.notifier) + .undoClose, + ); + } + + return true; + } else { + //Mark back as unhandled and navigator will pop + await SystemNavigator.pop(); + return false; + } + } else { + lastBackButtonPress.value = DateTime.now(); + ui_helper.showTabBackButtonMessage( + context, + tabCount, + _backButtonPressTimeout, + ); + + return true; + } + }, + child: _BrowserView( + sheetDisplayed: sheetDisplayed, + isFullscreen: tabInFullScreen, + ), + ), + ), + ); + }, + ), + builder: (context, ref, child) { + ref.listen(bottomSheetControllerProvider, (previous, next) { + if (sheetController.value != null) { + sheetController.value!.close(); + sheetController.value = null; + } + + if (next != null) { + final relativeSafeArea = MediaQuery.of( + context, + ).relativeSafeArea(); + sheetController + .value = Scaffold.of(context).showBottomSheet(( + context, + ) { + bool dismissOnThreshold( + DraggableScrollableNotification notification, + ) { + if (notification.extent <= 0.1) { ref .read(bottomSheetControllerProvider.notifier) .dismiss(); return true; } - if (overlayBuilder != null) { - ref - .read(overlayControllerProvider.notifier) - .dismiss(); - return true; - } - - if (tabState?.isFullScreen == true) { - await ref - .read(selectedTabSessionProvider) - .exitFullscreen(); - return true; - } - - //Make sure app bar is visible - showAppBar.value = true; - - if (tabState?.isLoading == true) { - lastBackButtonPress.value = null; - - final controller = ref.read( - selectedTabSessionProvider, - ); - - await controller.stopLoading(); - return true; - } else if (tabState?.readerableState.active == true) { - lastBackButtonPress.value = null; - - await ref - .read(readerableScreenControllerProvider.notifier) - .toggleReaderView(false); - - return true; - } else if (tabState?.historyState.canGoBack == true) { - lastBackButtonPress.value = null; - - final controller = ref.read( - selectedTabSessionProvider, - ); - - await controller.goBack(); - return true; - } - - //Go router has routes to go back to - if (context.canPop()) { - return true; - } - - if (ref - .read(tabRepositoryProvider.notifier) - .hasLaunchedFromIntent(tabState?.id)) { - //Mark back as unhandled and navigator will pop - await SystemNavigator.pop(); - return false; - } - - if (lastBackButtonPress.value != null && - DateTime.now().difference( - lastBackButtonPress.value!, - ) < - _backButtonPressTimeout) { - lastBackButtonPress.value = null; - - if (tabState != null && tabCount > 1) { - await ref - .read(tabRepositoryProvider.notifier) - .closeTab(tabState.id); - - if (context.mounted) { - ui_helper.showTabUndoClose( - context, - ref - .read(tabRepositoryProvider.notifier) - .undoClose, - ); - } - - return true; - } else { - //Mark back as unhandled and navigator will pop - await SystemNavigator.pop(); - return false; - } - } else { - lastBackButtonPress.value = DateTime.now(); - ui_helper.showTabBackButtonMessage( - context, - tabCount, - _backButtonPressTimeout, - ); - - return true; - } - }, - child: _BrowserView( - sheetDisplayed: sheetDisplayed, - isFullscreen: tabInFullScreen, - ), - ), - ), - ); - }, - ), - builder: (context, ref, child) { - ref.listen(bottomSheetControllerProvider, (previous, next) { - if (sheetController.value != null) { - sheetController.value!.close(); - sheetController.value = null; - } - - if (next != null) { - final relativeSafeArea = MediaQuery.of( - context, - ).relativeSafeArea(); - sheetController.value = Scaffold.of(context).showBottomSheet(( - context, - ) { - bool dismissOnThreshold( - DraggableScrollableNotification notification, - ) { - if (notification.extent <= 0.1) { - ref - .read(bottomSheetControllerProvider.notifier) - .dismiss(); - return true; + return false; } - return false; - } - - final sheet = switch (next) { - ViewTabsSheet() => - NotificationListener( - key: ValueKey(next), - onNotification: dismissOnThreshold, - child: _ViewTabsSheet(maxChildSize: relativeSafeArea), - ), - final EditUrlSheet parameter => - NotificationListener( - key: ValueKey(parameter), - onNotification: dismissOnThreshold, - child: _ViewUrlSheet( - initialTabState: parameter.tabState, - maxChildSize: relativeSafeArea, + final sheet = switch (next) { + ViewTabsSheet() => + NotificationListener( + key: ValueKey(next), + onNotification: dismissOnThreshold, + child: _ViewTabsSheet( + maxChildSize: relativeSafeArea, + ), ), - ), - }; + final EditUrlSheet parameter => + NotificationListener( + key: ValueKey(parameter), + onNotification: dismissOnThreshold, + child: _ViewUrlSheet( + initialTabState: parameter.tabState, + maxChildSize: relativeSafeArea, + ), + ), + }; - return sheet; - }); - } - }); + return sheet; + }); + } + }); - return child!; - }, + return child!; + }, + ), + floatingActionButton: ReaderAppearanceButton(), ), - floatingActionButton: ReaderAppearanceButton(), ), ), );