flutter_flow_util.dart 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  1. import 'dart:io';
  2. import 'package:flutter/foundation.dart' show kIsWeb;
  3. import 'package:flutter/material.dart';
  4. import 'package:flutter/services.dart';
  5. import 'package:collection/collection.dart';
  6. import 'package:from_css_color/from_css_color.dart';
  7. import 'dart:math' show pow, pi, sin;
  8. import 'package:intl/intl.dart';
  9. import 'package:json_path/json_path.dart';
  10. import 'package:timeago/timeago.dart' as timeago;
  11. import 'package:url_launcher/url_launcher.dart';
  12. import 'debug_util.dart';
  13. export 'debug_util.dart';
  14. export 'package:debug_panel_proto/debug_panel_proto.dart';
  15. export 'nav/serialization_util.dart';
  16. import '../main.dart';
  17. import 'lat_lng.dart';
  18. export 'keep_alive_wrapper.dart';
  19. export 'lat_lng.dart';
  20. export 'place.dart';
  21. export 'uploaded_file.dart';
  22. export '../app_state.dart';
  23. export 'flutter_flow_model.dart';
  24. export 'dart:math' show min, max;
  25. export 'dart:typed_data' show Uint8List;
  26. export 'dart:convert' show jsonEncode, jsonDecode;
  27. export 'package:intl/intl.dart';
  28. export 'package:page_transition/page_transition.dart';
  29. export 'internationalization.dart' show FFLocalizations;
  30. export 'nav/nav.dart';
  31. final RouteObserver<ModalRoute> routeObserver = RouteObserver<ModalRoute>();
  32. /// Observers notified of navigation on this app's navigator, in order.
  33. final List<NavigatorObserver> ffNavigatorObservers = <NavigatorObserver>[
  34. routeObserver,
  35. ];
  36. T valueOrDefault<T>(T? value, T defaultValue) =>
  37. (value is String && value.isEmpty) || value == null ? defaultValue : value;
  38. void _setTimeagoLocales() {
  39. timeago.setLocaleMessages('en', timeago.EnMessages());
  40. timeago.setLocaleMessages('en_short', timeago.EnShortMessages());
  41. }
  42. String dateTimeFormat(String format, DateTime? dateTime, {String? locale}) {
  43. if (dateTime == null) {
  44. return '';
  45. }
  46. if (format == 'relative') {
  47. _setTimeagoLocales();
  48. return timeago.format(dateTime, locale: locale, allowFromNow: true);
  49. }
  50. return DateFormat(format, locale).format(dateTime);
  51. }
  52. Theme wrapInMaterialDatePickerTheme(
  53. BuildContext context,
  54. Widget child, {
  55. required Color headerBackgroundColor,
  56. required Color headerForegroundColor,
  57. required TextStyle headerTextStyle,
  58. required Color pickerBackgroundColor,
  59. required Color pickerForegroundColor,
  60. required Color selectedDateTimeBackgroundColor,
  61. required Color selectedDateTimeForegroundColor,
  62. required Color actionButtonForegroundColor,
  63. required double iconSize,
  64. }) {
  65. final baseTheme = Theme.of(context);
  66. final dateTimeMaterialStateForegroundColor =
  67. WidgetStateProperty.resolveWith((states) {
  68. if (states.contains(WidgetState.disabled)) {
  69. return pickerForegroundColor.applyAlpha(0.60);
  70. }
  71. if (states.contains(WidgetState.selected)) {
  72. return selectedDateTimeForegroundColor;
  73. }
  74. if (states.isEmpty) {
  75. return pickerForegroundColor;
  76. }
  77. return null;
  78. });
  79. final dateTimeMaterialStateBackgroundColor =
  80. WidgetStateProperty.resolveWith((states) {
  81. if (states.contains(WidgetState.selected)) {
  82. return selectedDateTimeBackgroundColor;
  83. }
  84. return null;
  85. });
  86. return Theme(
  87. data: baseTheme.copyWith(
  88. colorScheme: baseTheme.colorScheme.copyWith(
  89. onSurface: pickerForegroundColor,
  90. ),
  91. disabledColor: pickerForegroundColor.applyAlpha(0.3),
  92. textTheme: baseTheme.textTheme.copyWith(
  93. headlineSmall: headerTextStyle,
  94. headlineMedium: headerTextStyle,
  95. ),
  96. iconTheme: baseTheme.iconTheme.copyWith(
  97. size: iconSize,
  98. ),
  99. textButtonTheme: TextButtonThemeData(
  100. style: ButtonStyle(
  101. foregroundColor: WidgetStatePropertyAll(
  102. actionButtonForegroundColor,
  103. ),
  104. overlayColor: WidgetStateProperty.resolveWith((states) {
  105. if (states.contains(WidgetState.hovered)) {
  106. return actionButtonForegroundColor.applyAlpha(0.04);
  107. }
  108. if (states.contains(WidgetState.focused) ||
  109. states.contains(WidgetState.pressed)) {
  110. return actionButtonForegroundColor.applyAlpha(0.12);
  111. }
  112. return null;
  113. })),
  114. ),
  115. datePickerTheme: DatePickerThemeData(
  116. backgroundColor: pickerBackgroundColor,
  117. headerBackgroundColor: headerBackgroundColor,
  118. headerForegroundColor: headerForegroundColor,
  119. weekdayStyle: baseTheme.textTheme.labelMedium!.copyWith(
  120. color: pickerForegroundColor,
  121. ),
  122. dayBackgroundColor: dateTimeMaterialStateBackgroundColor,
  123. todayBackgroundColor: dateTimeMaterialStateBackgroundColor,
  124. yearBackgroundColor: dateTimeMaterialStateBackgroundColor,
  125. dayForegroundColor: dateTimeMaterialStateForegroundColor,
  126. todayForegroundColor: dateTimeMaterialStateForegroundColor,
  127. yearForegroundColor: dateTimeMaterialStateForegroundColor,
  128. ),
  129. ),
  130. child: child,
  131. );
  132. }
  133. Theme wrapInMaterialTimePickerTheme(
  134. BuildContext context,
  135. Widget child, {
  136. required Color headerBackgroundColor,
  137. required Color headerForegroundColor,
  138. required TextStyle headerTextStyle,
  139. required Color pickerBackgroundColor,
  140. required Color pickerForegroundColor,
  141. required Color selectedDateTimeBackgroundColor,
  142. required Color selectedDateTimeForegroundColor,
  143. required Color actionButtonForegroundColor,
  144. required double iconSize,
  145. }) {
  146. final baseTheme = Theme.of(context);
  147. return Theme(
  148. data: baseTheme.copyWith(
  149. iconTheme: baseTheme.iconTheme.copyWith(
  150. size: iconSize,
  151. ),
  152. textButtonTheme: TextButtonThemeData(
  153. style: ButtonStyle(
  154. foregroundColor: WidgetStatePropertyAll(
  155. actionButtonForegroundColor,
  156. ),
  157. overlayColor: WidgetStateProperty.resolveWith((states) {
  158. if (states.contains(WidgetState.hovered)) {
  159. return actionButtonForegroundColor.applyAlpha(0.04);
  160. }
  161. if (states.contains(WidgetState.focused) ||
  162. states.contains(WidgetState.pressed)) {
  163. return actionButtonForegroundColor.applyAlpha(0.12);
  164. }
  165. return null;
  166. })),
  167. ),
  168. timePickerTheme: baseTheme.timePickerTheme.copyWith(
  169. backgroundColor: pickerBackgroundColor,
  170. hourMinuteTextColor: pickerForegroundColor,
  171. dialHandColor: selectedDateTimeBackgroundColor,
  172. dialTextColor: WidgetStateColor.resolveWith((states) =>
  173. states.contains(WidgetState.selected)
  174. ? selectedDateTimeForegroundColor
  175. : pickerForegroundColor),
  176. dayPeriodBorderSide: BorderSide(
  177. color: pickerForegroundColor,
  178. ),
  179. dayPeriodTextColor: WidgetStateColor.resolveWith((states) =>
  180. states.contains(WidgetState.selected)
  181. ? selectedDateTimeForegroundColor
  182. : pickerForegroundColor),
  183. dayPeriodColor: WidgetStateColor.resolveWith((states) =>
  184. states.contains(WidgetState.selected)
  185. ? selectedDateTimeBackgroundColor
  186. : Colors.transparent),
  187. entryModeIconColor: pickerForegroundColor,
  188. ),
  189. ),
  190. child: child,
  191. );
  192. }
  193. Future launchURL(String url) async {
  194. var uri = Uri.parse(url);
  195. try {
  196. await launchUrl(uri);
  197. } catch (e) {
  198. throw 'Could not launch $uri: $e';
  199. }
  200. }
  201. Color colorFromCssString(String color, {Color? defaultColor}) {
  202. try {
  203. return fromCssColor(color);
  204. } catch (_) {}
  205. return defaultColor ?? Colors.black;
  206. }
  207. enum FormatType {
  208. decimal,
  209. percent,
  210. scientific,
  211. compact,
  212. compactLong,
  213. custom,
  214. }
  215. enum DecimalType {
  216. automatic,
  217. periodDecimal,
  218. commaDecimal,
  219. }
  220. String formatNumber(
  221. num? value, {
  222. required FormatType formatType,
  223. DecimalType? decimalType,
  224. String? currency,
  225. bool toLowerCase = false,
  226. String? format,
  227. String? locale,
  228. }) {
  229. if (value == null) {
  230. return '';
  231. }
  232. var formattedValue = '';
  233. switch (formatType) {
  234. case FormatType.decimal:
  235. switch (decimalType!) {
  236. case DecimalType.automatic:
  237. formattedValue = NumberFormat.decimalPattern().format(value);
  238. break;
  239. case DecimalType.periodDecimal:
  240. if (currency != null) {
  241. formattedValue = NumberFormat('#,##0.00', 'en_US').format(value);
  242. } else {
  243. formattedValue = NumberFormat.decimalPattern('en_US').format(value);
  244. }
  245. break;
  246. case DecimalType.commaDecimal:
  247. if (currency != null) {
  248. formattedValue = NumberFormat('#,##0.00', 'es_PA').format(value);
  249. } else {
  250. formattedValue = NumberFormat.decimalPattern('es_PA').format(value);
  251. }
  252. break;
  253. }
  254. break;
  255. case FormatType.percent:
  256. formattedValue = NumberFormat.percentPattern().format(value);
  257. break;
  258. case FormatType.scientific:
  259. formattedValue = NumberFormat.scientificPattern().format(value);
  260. if (toLowerCase) {
  261. formattedValue = formattedValue.toLowerCase();
  262. }
  263. break;
  264. case FormatType.compact:
  265. formattedValue = NumberFormat.compact().format(value);
  266. break;
  267. case FormatType.compactLong:
  268. formattedValue = NumberFormat.compactLong().format(value);
  269. break;
  270. case FormatType.custom:
  271. final hasLocale = locale != null && locale.isNotEmpty;
  272. formattedValue =
  273. NumberFormat(format, hasLocale ? locale : null).format(value);
  274. }
  275. if (formattedValue.isEmpty) {
  276. return value.toString();
  277. }
  278. if (currency != null) {
  279. final currencySymbol = currency.isNotEmpty
  280. ? currency
  281. : NumberFormat.simpleCurrency().format(0.0).substring(0, 1);
  282. formattedValue = '$currencySymbol$formattedValue';
  283. }
  284. return formattedValue;
  285. }
  286. DateTime get getCurrentTimestamp => DateTime.now();
  287. DateTime dateTimeFromSecondsSinceEpoch(int seconds) {
  288. return DateTime.fromMillisecondsSinceEpoch(seconds * 1000);
  289. }
  290. extension DateTimeConversionExtension on DateTime {
  291. int get secondsSinceEpoch => (millisecondsSinceEpoch / 1000).round();
  292. }
  293. extension DateTimeComparisonOperators on DateTime {
  294. bool operator <(DateTime other) => isBefore(other);
  295. bool operator >(DateTime other) => isAfter(other);
  296. bool operator <=(DateTime other) => this < other || isAtSameMomentAs(other);
  297. bool operator >=(DateTime other) => this > other || isAtSameMomentAs(other);
  298. }
  299. T? castToType<T>(dynamic value) {
  300. if (value == null) {
  301. return null;
  302. }
  303. switch (T) {
  304. case double:
  305. // Doubles may be stored as ints in some cases.
  306. return value.toDouble() as T;
  307. case int:
  308. // Likewise, ints may be stored as doubles. If this is the case
  309. // (i.e. no decimal value), return the value as an int.
  310. if (value is num && value.toInt() == value) {
  311. return value.toInt() as T;
  312. }
  313. break;
  314. default:
  315. break;
  316. }
  317. return value as T;
  318. }
  319. dynamic getJsonField(
  320. dynamic response,
  321. String jsonPath, [
  322. bool isForList = false,
  323. ]) {
  324. final field = JsonPath(jsonPath).read(response);
  325. if (field.isEmpty) {
  326. return null;
  327. }
  328. if (field.length > 1) {
  329. return field.map((f) => f.value).toList();
  330. }
  331. final value = field.first.value;
  332. if (isForList) {
  333. return value is! Iterable
  334. ? [value]
  335. : (value is List ? value : value.toList());
  336. }
  337. return value;
  338. }
  339. Rect? getWidgetBoundingBox(BuildContext context) {
  340. try {
  341. final renderBox = context.findRenderObject() as RenderBox?;
  342. return renderBox!.localToGlobal(Offset.zero) & renderBox.size;
  343. } catch (_) {
  344. return null;
  345. }
  346. }
  347. bool get isAndroid => !kIsWeb && Platform.isAndroid;
  348. bool get isiOS => !kIsWeb && Platform.isIOS;
  349. bool get isWeb => kIsWeb;
  350. const kBreakpointSmall = 479.0;
  351. const kBreakpointMedium = 767.0;
  352. const kBreakpointLarge = 991.0;
  353. bool isMobileWidth(BuildContext context) =>
  354. MediaQuery.sizeOf(context).width < kBreakpointSmall;
  355. bool responsiveVisibility({
  356. required BuildContext context,
  357. bool phone = true,
  358. bool tablet = true,
  359. bool tabletLandscape = true,
  360. bool desktop = true,
  361. }) {
  362. final width = MediaQuery.sizeOf(context).width;
  363. if (width < kBreakpointSmall) {
  364. return phone;
  365. } else if (width < kBreakpointMedium) {
  366. return tablet;
  367. } else if (width < kBreakpointLarge) {
  368. return tabletLandscape;
  369. } else {
  370. return desktop;
  371. }
  372. }
  373. const kTextValidatorUsernameRegex = r'^[a-zA-Z][a-zA-Z0-9_-]{2,16}$';
  374. // https://stackoverflow.com/a/201378
  375. const kTextValidatorEmailRegex =
  376. "^(?:[a-zA-Z0-9!#\$%&\'*+/=?^_`{|}~-]+(?:\\.[a-zA-Z0-9!#\$%&\'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?|\\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-zA-Z0-9-]*[a-zA-Z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])\$";
  377. const kTextValidatorWebsiteRegex =
  378. r'(https?:\/\/)?(www\.)[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,10}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)|(https?:\/\/)?(www\.)?(?!ww)[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,10}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)';
  379. extension FFTextEditingControllerExt on TextEditingController? {
  380. String get text => this == null ? '' : this!.text;
  381. set text(String newText) => this?.text = newText;
  382. }
  383. extension IterableExt<T> on Iterable<T> {
  384. List<T> sortedList<S extends Comparable>(
  385. {S Function(T)? keyOf, bool desc = false}) {
  386. final sortedAscending = toList()
  387. ..sort(keyOf == null ? null : ((a, b) => keyOf(a).compareTo(keyOf(b))));
  388. if (desc) {
  389. return sortedAscending.reversed.toList();
  390. }
  391. return sortedAscending;
  392. }
  393. List<S> mapIndexed<S>(S Function(int, T) func) => toList()
  394. .asMap()
  395. .map((index, value) => MapEntry(index, func(index, value)))
  396. .values
  397. .toList();
  398. }
  399. void setAppLanguage(BuildContext context, String language) =>
  400. MyApp.of(context).setLocale(language);
  401. void setDarkModeSetting(BuildContext context, ThemeMode themeMode) =>
  402. MyApp.of(context).setThemeMode(themeMode);
  403. void showSnackbar(
  404. BuildContext context,
  405. String message, {
  406. bool loading = false,
  407. int duration = 4,
  408. }) {
  409. ScaffoldMessenger.of(context).hideCurrentSnackBar();
  410. ScaffoldMessenger.of(context).showSnackBar(
  411. SnackBar(
  412. content: Row(
  413. children: [
  414. if (loading)
  415. Padding(
  416. padding: EdgeInsetsDirectional.only(end: 10.0),
  417. child: Container(
  418. height: 20,
  419. width: 20,
  420. child: const CircularProgressIndicator(
  421. color: Colors.white,
  422. ),
  423. ),
  424. ),
  425. Text(message),
  426. ],
  427. ),
  428. duration: Duration(seconds: duration),
  429. ),
  430. );
  431. }
  432. extension FFStringExt on String {
  433. String maybeHandleOverflow({int? maxChars, String replacement = ''}) =>
  434. maxChars != null && length > maxChars
  435. ? replaceRange(maxChars, null, replacement)
  436. : this;
  437. String toCapitalization(TextCapitalization textCapitalization) {
  438. switch (textCapitalization) {
  439. case TextCapitalization.none:
  440. return this;
  441. case TextCapitalization.words:
  442. return split(' ').map(toBeginningOfSentenceCase).join(' ');
  443. case TextCapitalization.sentences:
  444. return toBeginningOfSentenceCase(this);
  445. case TextCapitalization.characters:
  446. return toUpperCase();
  447. }
  448. }
  449. }
  450. extension ListFilterExt<T> on Iterable<T?> {
  451. List<T> get withoutNulls => where((s) => s != null).map((e) => e!).toList();
  452. }
  453. extension MapFilterExtensions<T> on Map<String, T?> {
  454. Map<String, T> get withoutNulls => Map.fromEntries(
  455. entries
  456. .where((e) => e.value != null)
  457. .map((e) => MapEntry(e.key, e.value as T)),
  458. );
  459. }
  460. extension MapListContainsExt on List<dynamic> {
  461. bool containsMap(dynamic map) => map is Map
  462. ? any((e) => e is Map && const DeepCollectionEquality().equals(e, map))
  463. : contains(map);
  464. }
  465. extension ListDivideExt<T extends Widget> on Iterable<T> {
  466. Iterable<MapEntry<int, Widget>> get enumerate => toList().asMap().entries;
  467. List<Widget> divide(Widget t, {bool Function(int)? filterFn}) => isEmpty
  468. ? []
  469. : (enumerate
  470. .map((e) => [e.value, if (filterFn == null || filterFn(e.key)) t])
  471. .expand((i) => i)
  472. .toList()
  473. ..removeLast());
  474. List<Widget> around(Widget t) => addToStart(t).addToEnd(t);
  475. List<Widget> addToStart(Widget t) =>
  476. enumerate.map((e) => e.value).toList()..insert(0, t);
  477. List<Widget> addToEnd(Widget t) =>
  478. enumerate.map((e) => e.value).toList()..add(t);
  479. List<Padding> paddingTopEach(double val) =>
  480. map((w) => Padding(padding: EdgeInsets.only(top: val), child: w))
  481. .toList();
  482. }
  483. extension StatefulWidgetExtensions on State<StatefulWidget> {
  484. /// Check if the widget exist before safely setting state.
  485. void safeSetState(VoidCallback fn) {
  486. if (mounted) {
  487. // ignore: invalid_use_of_protected_member
  488. setState(fn);
  489. }
  490. }
  491. }
  492. // For iOS 16 and below, set the status bar color to match the app's theme.
  493. // https://github.com/flutter/flutter/issues/41067
  494. Brightness? _lastBrightness;
  495. void fixStatusBarOniOS16AndBelow(BuildContext context) {
  496. if (!isiOS) {
  497. return;
  498. }
  499. final brightness = Theme.of(context).brightness;
  500. if (_lastBrightness != brightness) {
  501. _lastBrightness = brightness;
  502. SystemChrome.setSystemUIOverlayStyle(
  503. SystemUiOverlayStyle(
  504. statusBarBrightness: brightness,
  505. systemStatusBarContrastEnforced: true,
  506. ),
  507. );
  508. }
  509. }
  510. extension ColorOpacityExt on Color {
  511. Color applyAlpha(double val) => withValues(alpha: val);
  512. }
  513. String roundTo(double value, int decimalPoints) {
  514. final power = pow(10, decimalPoints);
  515. return ((value * power).round() / power).toString();
  516. }
  517. double computeGradientAlignmentX(double evaluatedAngle) {
  518. evaluatedAngle %= 360;
  519. final rads = evaluatedAngle * pi / 180;
  520. double x;
  521. if (evaluatedAngle < 45 || evaluatedAngle > 315) {
  522. x = sin(2 * rads);
  523. } else if (45 <= evaluatedAngle && evaluatedAngle <= 135) {
  524. x = 1;
  525. } else if (135 <= evaluatedAngle && evaluatedAngle <= 225) {
  526. x = sin(-2 * rads);
  527. } else {
  528. x = -1;
  529. }
  530. return double.parse(roundTo(x, 2));
  531. }
  532. double computeGradientAlignmentY(double evaluatedAngle) {
  533. evaluatedAngle %= 360;
  534. final rads = evaluatedAngle * pi / 180;
  535. double y;
  536. if (evaluatedAngle < 45 || evaluatedAngle > 315) {
  537. y = -1;
  538. } else if (45 <= evaluatedAngle && evaluatedAngle <= 135) {
  539. y = sin(-2 * rads);
  540. } else if (135 <= evaluatedAngle && evaluatedAngle <= 225) {
  541. y = 1;
  542. } else {
  543. y = sin(2 * rads);
  544. }
  545. return double.parse(roundTo(y, 2));
  546. }
  547. extension ListUniqueExt<T> on Iterable<T> {
  548. List<T> unique(dynamic Function(T) getKey) {
  549. var distinctSet = <dynamic>{};
  550. var distinctList = <T>[];
  551. for (var item in this) {
  552. if (distinctSet.add(getKey(item))) {
  553. distinctList.add(item);
  554. }
  555. }
  556. return distinctList;
  557. }
  558. }
  559. String getCurrentRoute(BuildContext context) =>
  560. context.mounted ? MyApp.of(context).getRoute() : '';
  561. List<String> getCurrentRouteStack(BuildContext context) =>
  562. context.mounted ? MyApp.of(context).getRouteStack() : [];