Skip to content

Instantly share code, notes, and snippets.

@micimize
Created February 7, 2020 15:52
Show Gist options
  • Save micimize/5191af05191c027713a9cd1def3528db to your computer and use it in GitHub Desktop.
Save micimize/5191af05191c027713a9cd1def3528db to your computer and use it in GitHub Desktop.
/*
FULL cheaters_global_key.dart example,
contents:
* first defines the cheaters_global_key,
* then a few helpers
* then at the bottom a usage example
BEGIN cheaters_global_keys.dart
*/
import 'package:flutter/material.dart';
class CheatersGlobalKey extends InheritedWidget {
CheatersGlobalKey._({
this.cheatersKeys = const {},
Key key,
Widget child,
}) : super(key: key, child: child);
final Map<String, Key> cheatersKeys;
@override
bool updateShouldNotify(CheatersGlobalKey oldWidget) {
return cheatersKeys != oldWidget.cheatersKeys;
}
static Key of(
BuildContext context,
String label,
) {
assert(context != null);
final route = ModalRoute.of(context);
// only the current route cans safely render a GlobalKey
if (!route.isCurrent) {
return Key(label);
}
// can't render a GlobalKey if it ain't there -
// this ensures the current tab gets the global key
final CheatersGlobalKey result =
context.dependOnInheritedWidgetOfExactType<CheatersGlobalKey>();
if (result == null) {
return Key(label);
}
final keys = result?.cheatersKeys ?? {};
final key = keys[label] ??
Key(label); // we default to safely returning a regular key
return key;
}
}
class CheatersIndexedStack extends StatelessWidget {
CheatersIndexedStack({
Key key,
@required this.index,
@required this.cheatersKeys,
@required this.children,
List<String> contextLabels,
}) : this.contextLabels = contextLabels ??
List<String>.generate(
children.length,
(i) => 'cheater.index=$i',
),
super(key: key) {
assert(this.contextLabels.length == children.length);
}
final Set<String> cheatersKeys;
final List<String> contextLabels;
final List<Widget> children;
final int index;
Map<String, Key> keysFor(int childIndex) {
return index == childIndex
? Map.fromEntries(cheatersKeys.map(
(label) => MapEntry(
label,
_CheatersGlobalKey(label, contextLabels[childIndex]),
),
))
: null;
}
@override
Widget build(BuildContext context) {
return IndexedStack(
index: index,
children: children.map(withIndex((child, childIndex) {
return CheatersGlobalKey._(
// should change every tab switch
cheatersKeys: keysFor(childIndex),
child: child,
);
})).toList(),
);
}
}
class _CheatersGlobalKey extends GlobalObjectKey {
const _CheatersGlobalKey(String globalLabel, this.contextLabel)
: super(globalLabel);
final String contextLabel;
@override
String toString() {
return '[_CheatersGlobalKey#$hashCode value=$value, context=$contextLabel]';
}
}
/*
END cheaters_global_keys.dart
BEGIN pointless_helpers.dart
*/
typedef MapFn<A, B> = B Function(A a);
typedef IndexedMapFn<A, B> = B Function(A a, int index);
MapFn<A, B> withIndex<A, B>(IndexedMapFn<A, B> mapFn) {
int index = -1;
return (A a) => mapFn(a, ++index);
}
/*
END pointless_helpers.dart
BEGIN full_example.dart
*/
class TabBarExample extends StatefulWidget {
const TabBarExample({
Key key,
}) : super(key: key);
@override
_TabBarExampleState createState() => _TabBarExampleState();
}
class _TabBarExampleState extends State<TabBarExample>
with TickerProviderStateMixin {
TabController tabController;
int tabIndex = 0;
@override
void initState() {
super.initState();
tabController = TabController(
initialIndex: 0,
length: 3,
vsync: this,
);
}
static final tabs = [
'ONE',
"TWO",
"THREE",
]
.map((t) => Padding(
padding: const EdgeInsets.all(16.0),
child: Text(t),
))
.toList();
Widget tabOf(BuildContext context, int tabIndex) {
final key = CheatersGlobalKey.of(context, 'tabs');
return Column(mainAxisSize: MainAxisSize.max, children: [
TabBar(
key: CheatersGlobalKey.of(context, 'tabs'),
onTap: onTapNav,
controller: tabController,
tabs: tabs,
),
Container(
height: 300,
width: 300,
child: Center(
child: Text(
key.toString(),
style: Theme.of(context)
.textTheme
.button
.copyWith(color: Colors.white),
),
),
),
]);
}
Widget pageContent(BuildContext context, int selectedIndex) =>
CheatersIndexedStack(
index: selectedIndex,
cheatersKeys: const {'tabs'},
children: List.generate(
tabs.length,
(tabIndex) => Builder(
builder: (context) => tabOf(context, tabIndex),
),
),
);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey.shade800,
appBar: AppBar(
backgroundColor: Colors.indigo,
title: Text('AppBar!'),
),
body: pageContent(context, tabIndex),
);
}
void onTapNav(int index) {
if (mounted) {
setState(() {
tabIndex = index;
});
}
}
}
void main() {
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
home: TabBarExample(),
),
);
}
@micimize
Copy link
Author

micimize commented Feb 7, 2020

I was getting a pretty inscrutable error from this recently, and I think it was because there was a structure like ImplicitlyAnimated#CheatersGlobalKeyA(CheatersGlobalKeyB)). I believe the reason this started creating a problem was because flutter made widget disposal more optimal or something, but basically:

  • the top level widget is getting transitioned out, so it's children aren't updated
  • this means the nested global key isn't replaced, so a duplicate key error is thrown.

But, the tab animations seem fine, meaning there isn't actually a need to nest these global keys (for now)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment