From 7398e200c02c5dccf0f1f25d47903cb28fa28c6c Mon Sep 17 00:00:00 2001 From: syle Date: Mon, 16 Dec 2024 10:30:24 -0600 Subject: [PATCH] Android Predictive Back Support --- packages/flet/lib/src/controls/page.dart | 119 +++++++++++------- .../packages/flet/src/flet/core/page.py | 22 ++++ 2 files changed, 97 insertions(+), 44 deletions(-) diff --git a/packages/flet/lib/src/controls/page.dart b/packages/flet/lib/src/controls/page.dart index d80f19680..eba362894 100644 --- a/packages/flet/lib/src/controls/page.dart +++ b/packages/flet/lib/src/controls/page.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; @@ -276,6 +277,7 @@ class _PageControlState extends State with FletStoreMixin { widget.control.attrString("platform", "")!.toLowerCase(), orElse: () => defaultTargetPlatform); + _adaptive = widget.control.attrBool("adaptive"); _widgetsDesign = _adaptive == true && @@ -834,6 +836,7 @@ class ViewControl extends StatefulWidget { class _ViewControlState extends State with FletStoreMixin { final scaffoldKey = GlobalKey(); + DateTime ? currentBackPressTime; @override Widget build(BuildContext context) { @@ -1062,51 +1065,79 @@ class _ViewControlState extends State with FletStoreMixin { : widget.parent.attrString("darkTheme") != null ? parseTheme(widget.parent, "darkTheme", Brightness.dark) : parseTheme(widget.parent, "theme", Brightness.dark); + var backExitMessage = widget.parent.attrString("back_exit_message", "Press again to exit"); + var backExitEnabled = widget.parent.attrBool("back_exit_enabled", false)!; + + // For PopScope - https://docs.flutter.dev/release/breaking-changes/android-predictive-back + // We need to wrap Scaffold as the child -- this helps GOOGLE TV exit flet app properly + Future _allowBackPress() async { + DateTime now = DateTime.now(); + if (currentBackPressTime == null || now.difference(currentBackPressTime !) > Duration(seconds : 2)) { + currentBackPressTime = now; + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(backExitMessage!))); + return false; + } + // only exit(0) seems to work for Google TV Streamer + exit(0); // Back button hit twice exit program + } + + // This helper function takes scaffold as arguement, and if backExitEnabled + // is True, wraps Scaffold with Popscope, if false leaves Scaffold untouched + Widget _buildScaffoldWithPopScope(Widget child) { + return backExitEnabled + ? PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) async { + await _allowBackPress(); + }, + child: child, + ) : child; + } + + Widget scaffold = _buildScaffoldWithPopScope(Scaffold( + key: bar == null || bar is AppBarControl ? scaffoldKey : null, + backgroundColor: control.attrColor("bgcolor", context) ?? CupertinoTheme.of(context).scaffoldBackgroundColor, + appBar: bar is AppBarControl ? bar : null, + drawer: drawerView != null + ? NavigationDrawerControl( + control: drawerView.control, + children: drawerView.children, + parentDisabled: control.isDisabled, + parentAdaptive: adaptive, + backend: widget.backend) + : null, + onDrawerChanged: (opened) { + if (drawerView != null && !opened) { + widget.parent.state["drawerOpened"] = false; + dismissDrawer(drawerView.control.id); + } + }, + endDrawer: endDrawerView != null + ? NavigationDrawerControl( + control: endDrawerView.control, + children: endDrawerView.children, + parentDisabled: control.isDisabled, + parentAdaptive: adaptive, + backend: widget.backend) + : null, + onEndDrawerChanged: (opened) { + if (endDrawerView != null && !opened) { + widget.parent.state["endDrawerOpened"] = false; + dismissDrawer(endDrawerView.control.id); + } + }, + body: body, + bottomNavigationBar: bnb != null + ? createControl(control, bnb.id, control.isDisabled, + parentAdaptive: adaptive) + : null, + floatingActionButton: fab != null + ? createControl(control, fab.id, control.isDisabled, + parentAdaptive: adaptive) + : null, + floatingActionButtonLocation: fabLocation, + )); - Widget scaffold = Scaffold( - key: bar == null || bar is AppBarControl ? scaffoldKey : null, - backgroundColor: control.attrColor("bgcolor", context) ?? - CupertinoTheme.of(context).scaffoldBackgroundColor, - appBar: bar is AppBarControl ? bar : null, - drawer: drawerView != null - ? NavigationDrawerControl( - control: drawerView.control, - children: drawerView.children, - parentDisabled: control.isDisabled, - parentAdaptive: adaptive, - backend: widget.backend) - : null, - onDrawerChanged: (opened) { - if (drawerView != null && !opened) { - widget.parent.state["drawerOpened"] = false; - dismissDrawer(drawerView.control.id); - } - }, - endDrawer: endDrawerView != null - ? NavigationDrawerControl( - control: endDrawerView.control, - children: endDrawerView.children, - parentDisabled: control.isDisabled, - parentAdaptive: adaptive, - backend: widget.backend) - : null, - onEndDrawerChanged: (opened) { - if (endDrawerView != null && !opened) { - widget.parent.state["endDrawerOpened"] = false; - dismissDrawer(endDrawerView.control.id); - } - }, - body: body, - bottomNavigationBar: bnb != null - ? createControl(control, bnb.id, control.isDisabled, - parentAdaptive: adaptive) - : null, - floatingActionButton: fab != null - ? createControl(control, fab.id, control.isDisabled, - parentAdaptive: adaptive) - : null, - floatingActionButtonLocation: fabLocation, - ); var systemOverlayStyle = materialTheme.extension(); diff --git a/sdk/python/packages/flet/src/flet/core/page.py b/sdk/python/packages/flet/src/flet/core/page.py index b59c73b84..1775a59b5 100644 --- a/sdk/python/packages/flet/src/flet/core/page.py +++ b/sdk/python/packages/flet/src/flet/core/page.py @@ -576,6 +576,8 @@ def __init__( self.__dark_theme = None self.__locale_configuration = None self.__theme_mode = ThemeMode.SYSTEM # Default Theme Mode + self.__back_exit_enabled: Optional[bool] = False # Flutter Popscope + self.__back_exit_message: Optional[str] = None # Flutter Popscope snack bar message self.__pubsub: PubSubClient = PubSubClient(conn.pubsubhub, session_id) self.__client_storage: ClientStorage = ClientStorage(self) self.__session_storage: SessionStorage = SessionStorage(self) @@ -1634,6 +1636,26 @@ def client_storage(self) -> ClientStorage: def session(self) -> SessionStorage: return self.__session_storage + # back_exit_enabled + @property + def back_exit_enabled(self) -> Optional[bool]: + return self.__back_exit_enabled + + @back_exit_enabled.setter + def back_exit_enabled(self, value: Optional[bool]): + self.__back_exit_enabled = value if value is not None else False + self._set_attr("back_exit_enabled", self.__back_exit_enabled) + + # back_exit_message + @property + def back_exit_message(self) -> Optional[str]: + return self.__back_exit_message + + @back_exit_message.setter + def back_exit_message(self, value: Optional[str]): + self.__back_exit_message = value if value is not None else "Press again to exit" + self._set_attr("back_exit_message", self.__back_exit_message) + # theme_mode @property def theme_mode(self) -> Optional[ThemeMode]: