From 2f2d128e2b5330649995f3e9d7f8a6201f0c3079 Mon Sep 17 00:00:00 2001 From: Jonny Borges <35742643+jonataslaw@users.noreply.github.com> Date: Fri, 8 Mar 2024 16:51:49 -0300 Subject: [PATCH 1/3] fix: snackbar memory leak --- example/.metadata | 10 +++---- example/lib/main.dart | 13 +++++++-- example_nav2/android/local.properties | 4 +-- lib/get_navigation/src/root/get_root.dart | 3 ++ .../src/snackbar/snackbar_controller.dart | 28 ++++++++++++------- lib/get_utils/src/equality/equality.dart | 3 +- test/navigation/snackbar_test.dart | 27 +++++++++--------- 7 files changed, 53 insertions(+), 35 deletions(-) diff --git a/example/.metadata b/example/.metadata index b665bf909..b7bae1617 100644 --- a/example/.metadata +++ b/example/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "e1e47221e86272429674bec4f1bd36acc4fc7b77" + revision: "ba393198430278b6595976de84fe170f553cc728" channel: "stable" project_type: app @@ -13,11 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 - base_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 - platform: ios - create_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 - base_revision: e1e47221e86272429674bec4f1bd36acc4fc7b77 + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 # User provided section diff --git a/example/lib/main.dart b/example/lib/main.dart index 06af54cde..666ab6986 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -76,9 +76,16 @@ class First extends StatelessWidget { leading: IconButton( icon: const Icon(Icons.more), onPressed: () { - print('THEME CHANGED'); - Get.changeTheme( - Get.isDarkMode ? ThemeData.light() : ThemeData.dark()); + Get.snackbar( + 'title', + "message", + mainButton: + TextButton(onPressed: () {}, child: const Text('button')), + isDismissible: false, + ); + // print('THEME CHANGED'); + // Get.changeTheme( + // Get.isDarkMode ? ThemeData.light() : ThemeData.dark()); }, ), ), diff --git a/example_nav2/android/local.properties b/example_nav2/android/local.properties index cebbb54d3..79defc581 100644 --- a/example_nav2/android/local.properties +++ b/example_nav2/android/local.properties @@ -1,2 +1,2 @@ -sdk.dir=C:\\Users\\anike\\AppData\\Local\\Android\\sdk -flutter.sdk=C:\\flutter \ No newline at end of file +sdk.dir=/Users/jonatasborges/Library/Android/sdk +flutter.sdk=/Users/jonatasborges/flutter \ No newline at end of file diff --git a/lib/get_navigation/src/root/get_root.dart b/lib/get_navigation/src/root/get_root.dart index c8c7489a8..9dc62b216 100644 --- a/lib/get_navigation/src/root/get_root.dart +++ b/lib/get_navigation/src/root/get_root.dart @@ -45,6 +45,7 @@ class ConfigData { final Duration defaultDialogTransitionDuration; final Routing routing; final Map parameters; + final SnackBarQueue snackBarQueue = SnackBarQueue(); ConfigData({ required this.routingCallback, @@ -345,6 +346,8 @@ class GetRootState extends State with WidgetsBindingObserver { Get.resetInstance(clearRouteBindings: true); _controller = null; ambiguate(Engine.instance)!.removeObserver(this); + config.snackBarQueue.cancelAllJobs(); + config.snackBarQueue.disposeControllers(); } @override diff --git a/lib/get_navigation/src/snackbar/snackbar_controller.dart b/lib/get_navigation/src/snackbar/snackbar_controller.dart index 54a4c54ac..9f336f750 100644 --- a/lib/get_navigation/src/snackbar/snackbar_controller.dart +++ b/lib/get_navigation/src/snackbar/snackbar_controller.dart @@ -5,12 +5,14 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import '../../../get.dart'; +import '../root/get_root.dart'; class SnackbarController { - static final _snackBarQueue = _SnackBarQueue(); - static bool get isSnackbarBeingShown => _snackBarQueue._isJobInProgress; final key = GlobalKey(); + static bool get isSnackbarBeingShown => + GetRootState.controller.config.snackBarQueue.isJobInProgress; + late Animation _filterBlurAnimation; late Animation _filterColorAnimation; @@ -60,7 +62,7 @@ class SnackbarController { /// Only one GetSnackbar will be displayed at a time, and this method returns /// a future to when the snackbar disappears. Future show() { - return _snackBarQueue._addJob(this); + return GetRootState.controller.config.snackBarQueue.addJob(this); } void _cancelTimer() { @@ -348,15 +350,15 @@ class SnackbarController { } static Future cancelAllSnackbars() async { - await _snackBarQueue._cancelAllJobs(); + await GetRootState.controller.config.snackBarQueue.cancelAllJobs(); } static Future closeCurrentSnackbar() async { - await _snackBarQueue._closeCurrentJob(); + await GetRootState.controller.config.snackBarQueue.closeCurrentJob(); } } -class _SnackBarQueue { +class SnackBarQueue { final _queue = GetQueue(); final _snackbarList = []; @@ -365,22 +367,28 @@ class _SnackBarQueue { return _snackbarList.first; } - bool get _isJobInProgress => _snackbarList.isNotEmpty; + bool get isJobInProgress => _snackbarList.isNotEmpty; - Future _addJob(SnackbarController job) async { + Future addJob(SnackbarController job) async { _snackbarList.add(job); final data = await _queue.add(job._show); _snackbarList.remove(job); return data; } - Future _cancelAllJobs() async { + Future cancelAllJobs() async { await _currentSnackbar?.close(); _queue.cancelAllJobs(); _snackbarList.clear(); } - Future _closeCurrentJob() async { + Future disposeControllers() async { + for (var element in _snackbarList) { + element._controller.dispose(); + } + } + + Future closeCurrentJob() async { if (_currentSnackbar == null) return; await _currentSnackbar!.close(); } diff --git a/lib/get_utils/src/equality/equality.dart b/lib/get_utils/src/equality/equality.dart index e54c1b103..3c478d22f 100644 --- a/lib/get_utils/src/equality/equality.dart +++ b/lib/get_utils/src/equality/equality.dart @@ -6,9 +6,10 @@ mixin Equality { List get props; @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || runtimeType == other.runtimeType && + other is Equality && const DeepCollectionEquality().equals(props, other.props); } diff --git a/test/navigation/snackbar_test.dart b/test/navigation/snackbar_test.dart index f561b6dbd..29a84a405 100644 --- a/test/navigation/snackbar_test.dart +++ b/test/navigation/snackbar_test.dart @@ -110,7 +110,12 @@ void main() { const dismissDirection = DismissDirection.vertical; const snackBarTapTarget = Key('snackbar-tap-target'); - late final GetSnackBar getBar; + const GetSnackBar getBar = GetSnackBar( + message: 'bar1', + duration: Duration(seconds: 2), + isDismissible: true, + dismissDirection: dismissDirection, + ); await tester.pumpWidget(GetMaterialApp( home: Scaffold( @@ -121,12 +126,6 @@ void main() { GestureDetector( key: snackBarTapTarget, onTap: () { - getBar = const GetSnackBar( - message: 'bar1', - duration: Duration(seconds: 2), - isDismissible: true, - dismissDirection: dismissDirection, - ); Get.showSnackbar(getBar); }, behavior: HitTestBehavior.opaque, @@ -150,14 +149,14 @@ void main() { await tester.tap(find.byKey(snackBarTapTarget)); await tester.pumpAndSettle(); - expect(Get.isSnackbarOpen, true); - await tester.pump(const Duration(milliseconds: 500)); - expect(find.byWidget(getBar), findsOneWidget); - await tester.ensureVisible(find.byWidget(getBar)); - await tester.drag(find.byWidget(getBar), const Offset(0.0, 50.0)); - await tester.pump(const Duration(milliseconds: 500)); + // expect(Get.isSnackbarOpen, true); + // await tester.pump(const Duration(milliseconds: 500)); + // expect(find.byWidget(getBar), findsOneWidget); + // await tester.ensureVisible(find.byWidget(getBar)); + // await tester.drag(find.byWidget(getBar), const Offset(0.0, 50.0)); + // await tester.pump(const Duration(milliseconds: 500)); - expect(Get.isSnackbarOpen, false); + // expect(Get.isSnackbarOpen, false); }); testWidgets("test snackbar onTap", (tester) async { From 3a4dced526199585a6fd1325c031271f09e85092 Mon Sep 17 00:00:00 2001 From: Jonny Borges <35742643+jonataslaw@users.noreply.github.com> Date: Fri, 8 Mar 2024 19:23:39 -0300 Subject: [PATCH 2/3] fix dismissible tests --- example/lib/main.dart | 176 ++++-------------- lib/get_navigation/src/root/get_root.dart | 3 +- lib/get_navigation/src/snackbar/snackbar.dart | 8 + .../src/snackbar/snackbar_controller.dart | 13 +- test/navigation/snackbar_test.dart | 20 +- 5 files changed, 64 insertions(+), 156 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 666ab6986..ae0933561 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,171 +1,59 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -// void main() { -// runApp(const MyApp()); -// } - -// class MyApp extends StatelessWidget { -// const MyApp({Key? key}) : super(key: key); - -// @override -// Widget build(BuildContext context) { -// return GetMaterialApp( -// theme: ThemeData(useMaterial3: true), -// debugShowCheckedModeBanner: false, -// enableLog: true, -// logWriterCallback: Logger.write, -// initialRoute: AppPages.INITIAL, -// getPages: AppPages.routes, -// locale: TranslationService.locale, -// fallbackLocale: TranslationService.fallbackLocale, -// translations: TranslationService(), -// ); -// } -// } - -/// Nav 2 snippet void main() { - runApp(const MyApp()); + runApp(MyApp()); } class MyApp extends StatelessWidget { - const MyApp({Key? key}) : super(key: key); - +// This widget is the root of your application. @override Widget build(BuildContext context) { return GetMaterialApp( - getPages: [ - GetPage( - participatesInRootNavigator: true, - name: '/first', - page: () => const First()), - GetPage( - name: '/second', - page: () => const Second(), - transition: Transition.downToUp, - ), - GetPage( - name: '/third', - page: () => const Third(), - ), - ], + title: 'Scaffold demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: MyHomePage(), debugShowCheckedModeBanner: false, ); } } -class FirstController extends GetxController { - @override - void onClose() { - print('on close first'); - super.onClose(); - } -} - -class First extends StatelessWidget { - const First({Key? key}) : super(key: key); - +class MyHomePage extends StatelessWidget { @override Widget build(BuildContext context) { - print('First rebuild'); - Get.put(FirstController()); return Scaffold( appBar: AppBar( - title: const Text('page one'), - leading: IconButton( - icon: const Icon(Icons.more), + title: Text('Test'), + centerTitle: true, + backgroundColor: Colors.green, + ), + bottomNavigationBar: SizedBox( + width: double.infinity, + child: ElevatedButton( + child: Text('Tap me when Snackbar appears'), onPressed: () { - Get.snackbar( - 'title', - "message", - mainButton: - TextButton(onPressed: () {}, child: const Text('button')), - isDismissible: false, - ); - // print('THEME CHANGED'); - // Get.changeTheme( - // Get.isDarkMode ? ThemeData.light() : ThemeData.dark()); + print('This should clicked'); }, ), ), body: Center( - child: SizedBox( - height: 300, - width: 300, - child: ElevatedButton( - onPressed: () { - Get.toNamed('/second?id=123'); - }, - child: const Text('next screen'), - ), - ), - ), - ); - } -} - -class SecondController extends GetxController { - final textEdit = TextEditingController(); - @override - void onClose() { - print('on close second'); - textEdit.dispose(); - super.onClose(); - } -} - -class Second extends StatelessWidget { - const Second({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final controller = Get.put(SecondController()); - print('second rebuild'); - return Scaffold( - appBar: AppBar( - title: Text('page two ${Get.parameters["id"]}'), - ), - body: Center( - child: Column( - children: [ - Expanded( - child: TextField( - controller: controller.textEdit, - )), - SizedBox( - height: 300, - width: 300, - child: ElevatedButton( - onPressed: () {}, - child: const Text('next screen'), - ), - ), - ], - ), - ), - ); - } -} - -class Third extends StatelessWidget { - const Third({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Colors.red, - appBar: AppBar( - title: const Text('page three'), - ), - body: Center( - child: SizedBox( - height: 300, - width: 300, - child: ElevatedButton( - onPressed: () {}, - child: const Text('go to first screen'), - ), + child: ElevatedButton( + child: Text('Open Snackbar'), + onPressed: () { + Get.snackbar( + "Snackbar Showed", + "Please click the button on BottomNavigationBar", + icon: Icon(Icons.check, color: Colors.green), + backgroundColor: Colors.white, + snackStyle: SnackStyle.floating, + borderRadius: 20, + isDismissible: false, + snackPosition: SnackPosition.bottom, + margin: EdgeInsets.fromLTRB(50, 15, 50, 15), + ); + }, ), ), ); diff --git a/lib/get_navigation/src/root/get_root.dart b/lib/get_navigation/src/root/get_root.dart index 9dc62b216..eab7a6983 100644 --- a/lib/get_navigation/src/root/get_root.dart +++ b/lib/get_navigation/src/root/get_root.dart @@ -341,13 +341,12 @@ class GetRootState extends State with WidgetsBindingObserver { void onClose() { config.onDispose?.call(); Get.clearTranslations(); + config.snackBarQueue.disposeControllers(); RouterReportManager.instance.clearRouteKeys(); RouterReportManager.dispose(); Get.resetInstance(clearRouteBindings: true); _controller = null; ambiguate(Engine.instance)!.removeObserver(this); - config.snackBarQueue.cancelAllJobs(); - config.snackBarQueue.disposeControllers(); } @override diff --git a/lib/get_navigation/src/snackbar/snackbar.dart b/lib/get_navigation/src/snackbar/snackbar.dart index d51c775e9..29973476f 100644 --- a/lib/get_navigation/src/snackbar/snackbar.dart +++ b/lib/get_navigation/src/snackbar/snackbar.dart @@ -19,6 +19,13 @@ class GetSnackBar extends StatefulWidget { /// The title displayed to the user final String? title; + /// Defines how the snack bar area, including margin, will behave during hit testing. + /// + /// If this property is null and [margin] is not null, then [HitTestBehavior.deferToChild] is used by default. + /// + /// Please refer to [HitTestBehavior] for a detailed explanation of every behavior. + final HitTestBehavior? hitTestBehavior; + /// The direction in which the SnackBar can be dismissed. /// /// Default is [DismissDirection.down] when @@ -203,6 +210,7 @@ class GetSnackBar extends StatefulWidget { this.overlayColor = Colors.transparent, this.userInputForm, this.snackbarStatus, + this.hitTestBehavior, }) : super(key: key); @override diff --git a/lib/get_navigation/src/snackbar/snackbar_controller.dart b/lib/get_navigation/src/snackbar/snackbar_controller.dart index 9f336f750..4d408b8f9 100644 --- a/lib/get_navigation/src/snackbar/snackbar_controller.dart +++ b/lib/get_navigation/src/snackbar/snackbar_controller.dart @@ -245,6 +245,7 @@ class SnackbarController { snackbar.onHover?.call(snackbar, SnackHoverState.entered), onExit: (_) => snackbar.onHover?.call(snackbar, SnackHoverState.exited), child: GestureDetector( + behavior: snackbar.hitTestBehavior ?? HitTestBehavior.deferToChild, onTap: snackbar.onTap != null ? () => snackbar.onTap?.call(snackbar) : null, @@ -263,6 +264,7 @@ class SnackbarController { Widget _getDismissibleSnack(Widget child) { return Dismissible( + behavior: snackbar.hitTestBehavior ?? HitTestBehavior.opaque, direction: snackbar.dismissDirection ?? _getDefaultDismissDirection(), resizeDuration: null, confirmDismiss: (_) { @@ -382,10 +384,19 @@ class SnackBarQueue { _snackbarList.clear(); } - Future disposeControllers() async { + void disposeControllers() { + if (_currentSnackbar != null) { + _currentSnackbar?._removeOverlay(); + _currentSnackbar?._controller.dispose(); + _snackbarList.remove(_currentSnackbar); + } + + _queue.cancelAllJobs(); + for (var element in _snackbarList) { element._controller.dispose(); } + _snackbarList.clear(); } Future closeCurrentJob() async { diff --git a/test/navigation/snackbar_test.dart b/test/navigation/snackbar_test.dart index 29a84a405..42e270cd9 100644 --- a/test/navigation/snackbar_test.dart +++ b/test/navigation/snackbar_test.dart @@ -107,13 +107,15 @@ void main() { }); testWidgets("test snackbar dismissible", (tester) async { - const dismissDirection = DismissDirection.vertical; + const dismissDirection = DismissDirection.down; const snackBarTapTarget = Key('snackbar-tap-target'); const GetSnackBar getBar = GetSnackBar( + key: ValueKey('dismissible'), message: 'bar1', duration: Duration(seconds: 2), isDismissible: true, + snackPosition: SnackPosition.bottom, dismissDirection: dismissDirection, ); @@ -149,14 +151,14 @@ void main() { await tester.tap(find.byKey(snackBarTapTarget)); await tester.pumpAndSettle(); - // expect(Get.isSnackbarOpen, true); - // await tester.pump(const Duration(milliseconds: 500)); - // expect(find.byWidget(getBar), findsOneWidget); - // await tester.ensureVisible(find.byWidget(getBar)); - // await tester.drag(find.byWidget(getBar), const Offset(0.0, 50.0)); - // await tester.pump(const Duration(milliseconds: 500)); - - // expect(Get.isSnackbarOpen, false); + expect(Get.isSnackbarOpen, true); + await tester.pump(const Duration(milliseconds: 500)); + expect(find.byWidget(getBar), findsOneWidget); + await tester.ensureVisible(find.byWidget(getBar)); + await tester.drag(find.byType(Dismissible), const Offset(0.0, 50.0)); + await tester.pumpAndSettle(); + await tester.pump(const Duration(milliseconds: 500)); + expect(Get.isSnackbarOpen, false); }); testWidgets("test snackbar onTap", (tester) async { From 5155b2f9e483d788467d3c0722d9dac44b588c3f Mon Sep 17 00:00:00 2001 From: Jonny Borges <35742643+jonataslaw@users.noreply.github.com> Date: Fri, 8 Mar 2024 19:24:59 -0300 Subject: [PATCH 3/3] put example back --- example/lib/main.dart | 178 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 146 insertions(+), 32 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index ae0933561..5f1d73d42 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,59 +1,173 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +// void main() { +// runApp(const MyApp()); +// } + +// class MyApp extends StatelessWidget { +// const MyApp({Key? key}) : super(key: key); + +// @override +// Widget build(BuildContext context) { +// return GetMaterialApp( +// theme: ThemeData(useMaterial3: true), +// debugShowCheckedModeBanner: false, +// enableLog: true, +// logWriterCallback: Logger.write, +// initialRoute: AppPages.INITIAL, +// getPages: AppPages.routes, +// locale: TranslationService.locale, +// fallbackLocale: TranslationService.fallbackLocale, +// translations: TranslationService(), +// ); +// } +// } + +/// Nav 2 snippet void main() { - runApp(MyApp()); + runApp(const MyApp()); } class MyApp extends StatelessWidget { -// This widget is the root of your application. + const MyApp({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return GetMaterialApp( - title: 'Scaffold demo', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: MyHomePage(), + getPages: [ + GetPage( + participatesInRootNavigator: true, + name: '/first', + page: () => const First()), + GetPage( + name: '/second', + page: () => const Second(), + transition: Transition.downToUp, + ), + GetPage( + name: '/third', + page: () => const Third(), + ), + ], debugShowCheckedModeBanner: false, ); } } -class MyHomePage extends StatelessWidget { +class FirstController extends GetxController { + @override + void onClose() { + print('on close first'); + super.onClose(); + } +} + +class First extends StatelessWidget { + const First({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { + print('First rebuild'); + Get.put(FirstController()); return Scaffold( appBar: AppBar( - title: Text('Test'), - centerTitle: true, - backgroundColor: Colors.green, - ), - bottomNavigationBar: SizedBox( - width: double.infinity, - child: ElevatedButton( - child: Text('Tap me when Snackbar appears'), + title: const Text('page one'), + leading: IconButton( + icon: const Icon(Icons.more), onPressed: () { - print('This should clicked'); + Get.snackbar( + 'title', + "message", + mainButton: + TextButton(onPressed: () {}, child: const Text('button')), + isDismissible: true, + duration: Duration(seconds: 5), + snackbarStatus: (status) => print(status), + ); + // print('THEME CHANGED'); + // Get.changeTheme( + // Get.isDarkMode ? ThemeData.light() : ThemeData.dark()); }, ), ), body: Center( - child: ElevatedButton( - child: Text('Open Snackbar'), - onPressed: () { - Get.snackbar( - "Snackbar Showed", - "Please click the button on BottomNavigationBar", - icon: Icon(Icons.check, color: Colors.green), - backgroundColor: Colors.white, - snackStyle: SnackStyle.floating, - borderRadius: 20, - isDismissible: false, - snackPosition: SnackPosition.bottom, - margin: EdgeInsets.fromLTRB(50, 15, 50, 15), - ); - }, + child: SizedBox( + height: 300, + width: 300, + child: ElevatedButton( + onPressed: () { + Get.toNamed('/second?id=123'); + }, + child: const Text('next screen'), + ), + ), + ), + ); + } +} + +class SecondController extends GetxController { + final textEdit = TextEditingController(); + @override + void onClose() { + print('on close second'); + textEdit.dispose(); + super.onClose(); + } +} + +class Second extends StatelessWidget { + const Second({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final controller = Get.put(SecondController()); + print('second rebuild'); + return Scaffold( + appBar: AppBar( + title: Text('page two ${Get.parameters["id"]}'), + ), + body: Center( + child: Column( + children: [ + Expanded( + child: TextField( + controller: controller.textEdit, + )), + SizedBox( + height: 300, + width: 300, + child: ElevatedButton( + onPressed: () {}, + child: const Text('next screen'), + ), + ), + ], + ), + ), + ); + } +} + +class Third extends StatelessWidget { + const Third({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.red, + appBar: AppBar( + title: const Text('page three'), + ), + body: Center( + child: SizedBox( + height: 300, + width: 300, + child: ElevatedButton( + onPressed: () {}, + child: const Text('go to first screen'), + ), ), ), );