Skip to content

Instantly share code, notes, and snippets.

@clragon
Created August 26, 2023 02:10
Show Gist options
  • Select an option

  • Save clragon/18038b16e9bbdc5e14d4a6e5840517d1 to your computer and use it in GitHub Desktop.

Select an option

Save clragon/18038b16e9bbdc5e14d4a6e5840517d1 to your computer and use it in GitHub Desktop.
navgiator & overlay proxy
import 'package:flutter/material.dart';
import 'proxy.dart';
/// [BottomNavigationBar] has a weird assert that checks for [Overlay].
/// Proxies do not work with that. This test can only run properly in release mode.
void main() => runApp(const App());
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) => MaterialApp(
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
brightness: Brightness.dark,
seedColor: Colors.deepPurple,
),
useMaterial3: true,
),
initialRoute: '/1',
routes: {
'/1': (context) => const Page(index: 1),
'/2': (context) => const Page(index: 2),
'/3': (context) => const Page(index: 3),
'/4': (context) => const Page(index: 4),
},
builder: (context, child) => Proxy(
child: child!,
builder: (context, child) => Scaffold(
body: child!,
bottomNavigationBar: const NavBottomBar(),
),
),
);
}
class Page extends StatefulWidget {
const Page({
super.key,
required this.index,
});
final int index;
@override
State<Page> createState() => _PageState();
}
class _PageState extends State<Page> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Text('Page ${widget.index}'),
),
);
}
}
class NavBottomBar extends StatefulWidget {
const NavBottomBar({super.key});
@override
State<NavBottomBar> createState() => _NavBottomBarState();
}
class _NavBottomBarState extends State<NavBottomBar> {
int _index = 0;
@override
Widget build(BuildContext context) {
return BottomNavigationBar(
useLegacyColorScheme: false,
currentIndex: _index,
onTap: (index) {
switch (index) {
case 0:
Navigator.of(context).pushNamed('/1');
setState(() => _index = 0);
break;
case 1:
Navigator.of(context).pushNamed('/2');
setState(() => _index = 1);
break;
case 2:
Navigator.of(context).pushNamed('/3');
setState(() => _index = 2);
break;
case 3:
Navigator.of(context).pushNamed('/4');
setState(() => _index = 3);
break;
}
},
items: const [
BottomNavigationBarItem(
icon: Text('1'),
label: 'Page',
),
BottomNavigationBarItem(
icon: Text('2'),
label: 'Page',
),
BottomNavigationBarItem(
icon: Text('3'),
label: 'Page',
),
BottomNavigationBarItem(
icon: Text('4'),
label: 'Page',
),
],
);
}
}
import 'package:flutter/material.dart';
import 'proxy_grabber.dart';
import 'proxy_navigator.dart';
import 'proxy_overlay.dart';
/// A horrible abomination from the depths of hell.
///
/// Lifts the State of a [Navigator] or [Overlay] up to this widget,
/// so that children can access them.
///
/// This allows creating children which have access to the [Navigator] or [Overlay]
/// from their [BuildContext] without being inside of Routes and without needing a [GlobalKey].
class Proxy extends StatelessWidget {
const Proxy({
super.key,
required this.child,
this.builder,
});
final Widget child;
final TransitionBuilder? builder;
@override
Widget build(BuildContext context) {
return ProxyParent(
builder: (context, navigatorKey, overlayKey) => ProxyChild(
navigatorKey: navigatorKey,
overlayKey: overlayKey,
builder: builder,
child: child,
),
);
}
}
class ProxyParent extends StatelessWidget {
const ProxyParent({
super.key,
required this.builder,
});
final Widget Function(
BuildContext,
GlobalKey<NavigatorState>? navigatorKey,
GlobalKey<OverlayState>? overlayKey,
) builder;
@override
Widget build(BuildContext context) => NavigatorGrabber(
builder: (context, navigatorKey) => OverlayGrabber(
builder: (context, overlayKey) =>
builder(context, navigatorKey, overlayKey),
),
);
}
class ProxyChild extends StatefulWidget {
const ProxyChild({
super.key,
required this.child,
required this.builder,
required this.navigatorKey,
required this.overlayKey,
});
final Widget child;
final TransitionBuilder? builder;
final GlobalKey<NavigatorState>? navigatorKey;
final GlobalKey<OverlayState>? overlayKey;
@override
State<ProxyChild> createState() => _ProxyChildState();
}
class _ProxyChildState extends State<ProxyChild> {
final GlobalKey<_ReparentState> _reparentKey = GlobalKey<_ReparentState>();
@override
Widget build(BuildContext context) {
bool hasProxies = widget.navigatorKey != null || widget.overlayKey != null;
Widget child = _Reparent(
key: _reparentKey,
child: widget.child,
);
if (hasProxies) {
child = widget.builder?.call(context, child) ?? child;
}
if (widget.navigatorKey != null) {
child = ProxyNavigator(
navigatorKey: widget.navigatorKey,
child: child,
);
}
if (widget.overlayKey != null) {
child = ProxyOverlay(
overlayKey: widget.overlayKey,
child: child,
);
}
return child;
}
}
class _Reparent extends StatefulWidget {
const _Reparent({
required GlobalKey super.key,
required this.child,
});
final Widget child;
@override
State<_Reparent> createState() => _ReparentState();
}
class _ReparentState extends State<_Reparent> {
@override
Widget build(BuildContext context) => widget.child;
}
import 'package:flutter/material.dart';
/// A horrible abomination from the depths of hell.
///
/// Looks for the nearest [Navigator] Widget and returns its [GlobalKey].
class NavigatorGrabber extends GlobalKeyGrabber<Navigator, NavigatorState> {
const NavigatorGrabber({
Key? key,
required Widget Function(
BuildContext context,
GlobalKey<NavigatorState>? key,
) builder,
int searchDepth = 100,
}) : super(
key: key,
builder: builder,
searchDepth: searchDepth,
);
}
/// A horrible abomination from the depths of hell.
///
/// Looks for the nearest [Overlay] Widget and returns its [GlobalKey].
class OverlayGrabber extends GlobalKeyGrabber<Overlay, OverlayState> {
const OverlayGrabber({
Key? key,
required Widget Function(
BuildContext context,
GlobalKey<OverlayState>? key,
) builder,
int searchDepth = 100,
}) : super(
key: key,
builder: builder,
searchDepth: searchDepth,
);
}
/// A horrible abomination from the depths of hell.
///
/// A widget that looks for the nearest [T] Widget and returns its [GlobalKey] of type [R].
class GlobalKeyGrabber<T extends Widget, R extends State<StatefulWidget>>
extends StatefulWidget {
const GlobalKeyGrabber({
super.key,
required this.builder,
this.searchDepth = 100,
});
final Widget Function(BuildContext context, GlobalKey<R>? key) builder;
final int searchDepth;
@override
State<GlobalKeyGrabber<T, R>> createState() => _GlobalKeyGrabberState<T, R>();
}
class _GlobalKeyGrabberState<T extends Widget, R extends State<StatefulWidget>>
extends State<GlobalKeyGrabber<T, R>> {
final GlobalKey<_GrabberTargetState> _searchKey =
GlobalKey<_GrabberTargetState>();
GlobalKey<R>? _key;
void _findKey(Element element, int depth) {
if (element.widget is T) {
setState(() {
_key = element.widget.key as GlobalKey<R>?;
});
} else {
if (depth > widget.searchDepth) {
throw FlutterError.fromParts(
<DiagnosticsNode>[
ErrorSummary(
'Could not find GlobalKey $R of Widget $T after searching $depth elements',
),
ErrorDescription(
'Could not find GlobalKey $R of Widget $T after searching $depth elements. '
'This usually means that you are trying to find a GlobalKey '
'of a widget that is not in the widget tree. ',
),
],
);
}
element.visitChildElements((element) => _findKey(element, depth + 1));
}
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_findKey(_searchKey.currentContext as Element, 0);
});
}
@override
Widget build(BuildContext context) {
return _GrabberTarget(
key: _searchKey,
child: widget.builder(context, _key),
);
}
}
class _GrabberTarget extends StatefulWidget {
const _GrabberTarget({
required super.key,
required this.child,
});
final Widget child;
@override
State<_GrabberTarget> createState() => _GrabberTargetState();
}
class _GrabberTargetState extends State<_GrabberTarget> {
@override
Widget build(BuildContext context) {
return widget.child;
}
}
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
/// A horrible abomination from the depths of hell.
///
/// Routes all calls to a [Navigator] to a [Navigator] with a different key.
/// This is needed when we want to lift a [NavigatorState] up in the [BuildContext]
/// so that we can wrap our Routes with Widgets that need access to the [NavigatorState],
/// without needing a [GlobalKey] to the [Navigator].
class ProxyNavigator extends Navigator {
const ProxyNavigator({
super.key,
required this.navigatorKey,
required this.child,
});
final GlobalKey<NavigatorState>? navigatorKey;
final Widget child;
@override
NavigatorState createState() => ProxyNavigatorState();
}
class ProxyNavigatorState extends NavigatorState {
@override
ProxyNavigator get widget => super.widget as ProxyNavigator;
NavigatorState? get maybeTarget => widget.navigatorKey?.currentState;
NavigatorState get target {
if (widget.navigatorKey == null) {
throw FlutterError.fromParts(
<DiagnosticsNode>[
ErrorSummary('ProxyNavigatorState has no navigatorKey'),
ErrorDescription(
'ProxyNavigatorState was called but has no navigatorKey. '
'This usually means that you are trying to use a ProxyNavigator '
'before a valid navigatorKey has been assigned.',
),
],
);
}
return widget.navigatorKey!.currentState!;
}
@override
RestorationBucket? get bucket => target.bucket;
@override
bool canPop() => target.canPop();
@override
Ticker createTicker(TickerCallback onTick) => target.createTicker(onTick);
@override
void didStartUserGesture() => target.didStartUserGesture();
@override
void didStopUserGesture() => target.didStopUserGesture();
@override
// ignore: must_call_super
void didToggleBucket(RestorationBucket? oldBucket) =>
target.didToggleBucket(oldBucket);
@override
void didUpdateRestorationId() => target.didUpdateRestorationId();
@override
void finalizeRoute(Route route) => target.finalizeRoute(route);
@override
FocusNode get focusNode => target.focusNode;
@override
@Deprecated('Use focusNode.enclosingScope! instead. '
'This feature was deprecated after v3.1.0-0.0.pre.')
FocusScopeNode get focusScopeNode => target.focusScopeNode;
@override
Future<bool> maybePop<T extends Object?>([T? result]) =>
target.maybePop(result);
@override
OverlayState? get overlay => target.overlay;
@override
void pop<T extends Object?>([T? result]) => target.pop(result);
@override
Future<T?> popAndPushNamed<T extends Object?, TO extends Object?>(
String routeName,
{TO? result,
Object? arguments}) =>
target.popAndPushNamed(
routeName,
result: result,
arguments: arguments,
);
@override
void popUntil(RoutePredicate predicate) => target.popUntil(predicate);
@override
Future<T?> push<T extends Object?>(Route<T> route) => target.push(route);
@override
Future<T?> pushAndRemoveUntil<T extends Object?>(
Route<T> newRoute, RoutePredicate predicate) =>
target.pushAndRemoveUntil(
newRoute,
predicate,
);
@override
Future<T?> pushNamed<T extends Object?>(String routeName,
{Object? arguments}) =>
target.pushNamed(
routeName,
arguments: arguments,
);
@override
Future<T?> pushNamedAndRemoveUntil<T extends Object?>(
String newRouteName, RoutePredicate predicate,
{Object? arguments}) =>
target.pushNamedAndRemoveUntil(
newRouteName,
predicate,
arguments: arguments,
);
@override
Future<T?> pushReplacement<T extends Object?, TO extends Object?>(
Route<T> newRoute,
{TO? result}) =>
target.pushReplacement(
newRoute,
result: result,
);
@override
Future<T?> pushReplacementNamed<T extends Object?, TO extends Object?>(
String routeName,
{TO? result,
Object? arguments}) =>
target.pushReplacementNamed(
routeName,
result: result,
arguments: arguments,
);
@override
void registerForRestoration(
RestorableProperty<Object?> property, String restorationId) =>
target.registerForRestoration(property, restorationId);
@override
void removeRoute(Route route) => target.removeRoute(route);
@override
void removeRouteBelow(Route anchorRoute) =>
target.removeRouteBelow(anchorRoute);
@override
void replace<T extends Object?>(
{required Route oldRoute, required Route<T> newRoute}) =>
target.replace(
oldRoute: oldRoute,
newRoute: newRoute,
);
@override
void replaceRouteBelow<T extends Object?>(
{required Route anchorRoute, required Route<T> newRoute}) =>
target.replaceRouteBelow(
anchorRoute: anchorRoute,
newRoute: newRoute,
);
@override
String restorablePopAndPushNamed<T extends Object?, TO extends Object?>(
String routeName,
{TO? result,
Object? arguments}) =>
target.restorablePopAndPushNamed(
routeName,
result: result,
arguments: arguments,
);
@override
String restorablePush<T extends Object?>(
RestorableRouteBuilder<T> routeBuilder,
{Object? arguments}) =>
target.restorablePush(
routeBuilder,
arguments: arguments,
);
@override
String restorablePushAndRemoveUntil<T extends Object?>(
RestorableRouteBuilder<T> newRouteBuilder, RoutePredicate predicate,
{Object? arguments}) =>
target.restorablePushAndRemoveUntil(
newRouteBuilder,
predicate,
arguments: arguments,
);
@override
String restorablePushNamed<T extends Object?>(String routeName,
{Object? arguments}) =>
target.restorablePushNamed(
routeName,
arguments: arguments,
);
@override
String restorablePushNamedAndRemoveUntil<T extends Object?>(
String newRouteName, RoutePredicate predicate,
{Object? arguments}) =>
target.restorablePushNamedAndRemoveUntil(
newRouteName,
predicate,
arguments: arguments,
);
@override
String restorablePushReplacement<T extends Object?, TO extends Object?>(
RestorableRouteBuilder<T> routeBuilder,
{TO? result,
Object? arguments}) =>
target.restorablePushReplacement(
routeBuilder,
result: result,
arguments: arguments,
);
@override
String restorablePushReplacementNamed<T extends Object?, TO extends Object?>(
String routeName,
{TO? result,
Object? arguments}) =>
target.restorablePushReplacementNamed(
routeName,
result: result,
arguments: arguments,
);
@override
String restorableReplace<T extends Object?>(
{required Route oldRoute,
required RestorableRouteBuilder<T> newRouteBuilder,
Object? arguments}) =>
target.restorableReplace(
oldRoute: oldRoute,
newRouteBuilder: newRouteBuilder,
arguments: arguments,
);
@override
String restorableReplaceRouteBelow<T extends Object?>(
{required Route anchorRoute,
required RestorableRouteBuilder<T> newRouteBuilder,
Object? arguments}) =>
target.restorableReplaceRouteBelow(
anchorRoute: anchorRoute,
newRouteBuilder: newRouteBuilder,
arguments: arguments,
);
@override
String? get restorationId => maybeTarget?.restorationId;
@override
bool get restorePending => maybeTarget?.restorePending ?? false;
@override
// ignore: must_call_super
void restoreState(RestorationBucket? oldBucket, bool initialRestore) =>
target.restoreState(
oldBucket,
initialRestore,
);
@override
void unregisterFromRestoration(RestorableProperty<Object?> property) =>
target.unregisterFromRestoration(property);
@override
bool get userGestureInProgress => target.userGestureInProgress;
@override
ValueNotifier<bool> get userGestureInProgressNotifier =>
target.userGestureInProgressNotifier;
@override
Widget build(BuildContext context) => widget.child;
}
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
/// A horrible abomination from the depths of hell.
///
/// Routes all calls to an [Overlay] to an [Overlay] with a different key.
/// This is needed when we want to lift an [OverlayState] up in the [BuildContext]
/// so that we can wrap our Routes with Widgets that need access to the [OverlayState],
/// without needing a [GlobalKey] to the [Overlay].
class ProxyOverlay extends Overlay {
const ProxyOverlay({
super.key,
required this.overlayKey,
required this.child,
});
final GlobalKey<OverlayState>? overlayKey;
final Widget child;
@override
OverlayState createState() => ProxyOverlayState();
}
class ProxyOverlayState extends OverlayState {
@override
ProxyOverlay get widget => super.widget as ProxyOverlay;
OverlayState get target {
if (widget.overlayKey == null) {
throw FlutterError.fromParts(
<DiagnosticsNode>[
ErrorSummary('ProxyOverlayState has no overlayKey'),
ErrorDescription(
'ProxyOverlayState was called but has no overlayKey. '
'This usually means that you are trying to use a ProxyOverlay '
'before a valid overlayKey has been assigned.',
),
],
);
}
return widget.overlayKey!.currentState!;
}
@override
Ticker createTicker(TickerCallback onTick) => target.createTicker(onTick);
@override
bool debugIsVisible(OverlayEntry entry) => target.debugIsVisible(entry);
@override
void insert(
OverlayEntry entry, {
OverlayEntry? below,
OverlayEntry? above,
}) =>
target.insert(
entry,
below: below,
above: above,
);
@override
void insertAll(
Iterable<OverlayEntry> entries, {
OverlayEntry? below,
OverlayEntry? above,
}) =>
target.insertAll(
entries,
below: below,
above: above,
);
@override
void rearrange(
Iterable<OverlayEntry> newEntries, {
OverlayEntry? below,
OverlayEntry? above,
}) =>
target.rearrange(
newEntries,
below: below,
above: above,
);
@override
Widget build(BuildContext context) => widget.child;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment