Last updated:
0 purchases
keyed collection widgets
These are replacements to BottomNavigationBar, IndexedStack, and TabController that use
item keys instead if numeric indexes.
Problem #
With traditional widgets, you write something like
const tabFavorites = 0;
const tabSearch = 1;
// ...
if (tabIndex == tabFavorites) {
// ...
}
copied to clipboard
If items in your bar can change, you get an error-prone conversion from indexes to meanings.
Also with a mature architecture you tend to use enum for your tabs, and even with constant bar items
you must write code to convert between enum and int.
Solution #
This package provides widgets to be used with any type instead of int. In most cases you will
use enum.
Some advantages of enum over indexes:
No way for the value to fall out of range.
Easier debugging with IDE tools.
You will never use magic numbers for indexes.
No need to synchronize the order of children between different widgets.
String is also a good type to use with these widgets if you have dynamic or potentially
unlimited tabs (like in a browser or a document editor) but still want meaningful keys
instead of indexes.
KeyedBottomNavigationBar and KeyedStack #
This example uses an enum for selectable navigation items:
This example is runnable. Download the repository and open the example project.
Then run nav_stack.dart
enum TabEnum { favorites, search }
class _MyScreenState extends State<MyScreen> {
TabEnum _tab = TabEnum.favorites;
@override
Widget build(BuildContext context) {
// This is a simplified example: IndexedStack and KeyedStack are only
// meaningful if they contain stateful widgets to preserve state
// between switches.
return Scaffold(
body: KeyedStack<TabEnum>(
itemKey: _tab,
children: const {
TabEnum.favorites: Center(
key: ValueKey('favorites_pane'),
child: Text('Favorites'),
),
TabEnum.search: Center(
key: ValueKey('search_pane'),
child: Text('Search'),
),
},
),
bottomNavigationBar: KeyedBottomNavigationBar<TabEnum>(
currentItemKey: _tab,
items: const {
TabEnum.favorites: BottomNavigationBarItem(
icon: Icon(Icons.star),
label: 'Favorites',
),
TabEnum.search: BottomNavigationBarItem(
icon: Icon(Icons.search),
label: 'Search',
),
},
onTap: (tab) => setState(() {
_tab = tab;
}),
),
);
}
}
copied to clipboard
KeyedBottomNavigationBar and KeyedStack support all the arguments of their
traditional counterparts.
The only difference is that current keys are required and do not default to first element.
KeyedTabController, KeyedTabBar, KeyedTabBarView #
Minimal Example #
This example uses enum for tabs:
This example is runnable. Download the repository and open the example project.
Then run tabs.dart
enum MyTab { one, two, three }
class _MyScreenState extends State<MyScreen> with TickerProviderStateMixin {
late final _tabController = KeyedTabController<TabEnum>(
initialKey: TabEnum.three,
keys: [TabEnum.one, TabEnum.two, TabEnum.three],
vsync: this,
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('${_tabController.currentKey}'),
bottom: KeyedTabBar(
tabs: {
for (final key in _tabController.keys) key: Tab(text: '$key'),
},
controller: _tabController,
),
),
body: KeyedTabBarView(
children: {
for (final key in _tabController.keys)
key: Center(child: Text('$key content')),
},
controller: _tabController,
),
);
}
}
copied to clipboard
Redundant Children #
The ordinary TabBar and TabBarView must have exactly as many children as their controller
is set to. This means that if you need to hide some tabs, there must be three locations in your
code to know that:
The code that updates the controller.
The code that creates TabBar widget with tab headers.
The code that creates TabBarView widget with tab contents.
This is extremely error-prone.
With this package, KeyedTabBar and KeyedTabBarView have maps of children,
so they can contain more widgets than the controller wants to show.
This means that you can unconditionally pass all children for all possible tabs to them,
and the only location in your code that needs to know what tabs to show is the code that
updates the controller.
Using KeyedTabController as TabController #
KeyedTabController implements TabController and is immediately usable as one.
If you ever need to get the tab index or select a tab by index, do it as you normally would.
UnanimatedKeyedTabController #
With ordinary TabController, you need
TickerProvider
to create it. And this limits the usage. You must create TabController in a widget.
If you want your BLoC or other business logic code to be aware of tabs or control them,
it may be tricky to pass the controller there.
This package provides UnanimatedKeyedTabController which has the logic core
for KeyedTabController, but not its animation. You can create this controller anywhere
and then add the animation in your widget.
Create it like this:
final unanimatedController = UnanimatedKeyedTabController<TabEnum>(
keys: [TabEnum.one, TabEnum.two, TabEnum.three],
initialKey: TabEnum.three,
);
copied to clipboard
Then create KeyedTabController in your widget:
class _MyScreenState extends State<MyScreen> with TickerProviderStateMixin {
late final _tabController = KeyedTabController<TabEnum>.fromUnanimated(
controller: unanimatedController,
vsync: this,
);
// ...
copied to clipboard
This binds the two controllers. If you change the tab via UnanimatedKeyedTabController,
then KeyedTabController gets updated, and the tab change is animated in the UI.
And if the user changes the tab by interacting with it, both controllers get updated.
Mutable Tabs and Animation Duration #
Ordinary TabController has a fixed length and animationDuration. If you need to change them,
you must create a new controller and replace it everywhere.
KeyedTabController has these mutable.
You can change the tabs at any time by setting KeyedTabController.keys
property. If the currently selected tab also exists in the new set, its selection is preserved,
otherwise the first new tab gets selected.
This is possible because KeyedTabController does not extend but contains TabController
and so it can re-create its internal TabController with different parameters
without disturbing its own listeners.
TickerProviderStateMixin vs SingleTickerProviderStateMixin #
In Flutter's tab examples, you often see the widget's State created with
SingleTickerProviderStateMixin. This only allows one TabController to be created in it.
However, KeyedTabController re-creates its TabController if you change keys
or animationDuration, so it will break if created with SingleTickerProviderStateMixin.
You should use TickerProviderStateMixin for your widgets instead. It allows many TabController
objects to be created with it.
DefaultKeyedTabController #
Flutter provides
DefaultTabController
widget which accepts the number of tabs, creates a TabController and provides it to
all tab-related widgets under it.
It has the following advantages:
It is declarative. You don't have to manage your TabController and dispose it.
It allows to change the number of tabs. You just change the length passed to this widget,
and it re-creates the TabController,
and all widgets under it are updated for the new number of tabs.
This is matched by DefaultKeyedTabController. Although tabs get mutable with this package,
the advantage #1 still stands.
This widget comes in two forms:
fromKeys
DefaultKeyedTabController.fromKeys(
keys: [TabEnum.one, TabEnum.two],
child: ...
),
copied to clipboard
Use this when you know the keys to show in your widget.
fromUnanimated
DefaultKeyedTabController.fromUnanimated(
controller: unanimatedController,
child: ...
),
copied to clipboard
Use this if you use UnanimatedKeyedTabController.
Creating Widgets for DefaultKeyedTabController
In Flutter, both TabBar and TabBarView widgets can be created without
the controller argument. In this case, they rely on DefaultTabController widget
present in the tree above them and break if it is missing.
This is error-prone because the controller argument may simply be forgotten,
and this cannot be detected at compile time.
In this package, the controller argument to KeyedTabBar and KeyedTabBarView is required.
To rely on the DefaultKeyedTabController, use .withDefaultController static methods
of those widgets instead of their default constructors.
There is still no check at compile time that the default controller is present in the tree,
but at least you must explicitly declare that you want it and not just have forgotten
to pass the controller.
When to use DefaultKeyedTabController
All things equal, prefer DefaultKeyedTabController over manual KeyedTabController
creation. This is because that widget will dispose the controller for you when it is not needed
anymore.
Advanced Example #
This example shows:
Changing the set of tabs without re-creating a controller.
Programmatically activating a tab by its key.
Programmatically activating a tab by its index.
Changing the animation duration without re-creating a controller.
DefaultKeyedTabController widgets.
UnanimatedKeyedTabController.
Replacing the UnanimatedKeyedTabController that a KeyedTabController is linked to.
This example is runnable. Download the repository and open the example project.
Then run nav_stack_tabs.dart
enum_map #
Although enum enhances type safety for tabs, it is still not absolute.
In widgets, you may still forget to use all keys in children map and only know that at runtime.
You can make this compile-time safe by using enum_map
package that generates maps
that are guaranteed to have all keys at compile time (see that package's README for more info):
@unmodifiableEnumMap // CHANGED
enum TabEnum { one, two, three }
class _MyScreenState extends State<MyScreen> with TickerProviderStateMixin {
late final _tabController = KeyedTabController<TabEnum>(
initialKey: TabEnum.three,
keys: [TabEnum.one, TabEnum.two, TabEnum.three],
vsync: this,
);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('${_tabController.currentKey}'),
bottom: KeyedTabBar(
controller: _tabController,
tabs: const UnmodifiableTabEnumMap( // CHANGED
one: Tab(text: 'One'), // CHANGED
two: Tab(text: 'Two'), // CHANGED
three: Tab(text: 'Three'), // CHANGED
), // CHANGED
),
),
body: KeyedTabBarView(
controller: _tabController,
children: const UnmodifiableTabEnumMap( // CHANGED
one: Center(child: Text('One content')), // CHANGED
two: Center(child: Text('Two content')), // CHANGED
three: Center(child: Text('Three content')), // CHANGED
), // CHANGED
),
);
}
}
copied to clipboard
Support Chat #
Do you have any questions? Feel free to ask in the Telegram Support Chat.
Or even just join to say 'Hi!'. I like to hear from the users.
For personal and professional use. You cannot resell or redistribute these repositories in their original state.
There are no reviews.