sliver_tabbar_with_mixed_list 1.0.1
sliver_tabbar_with_mixed_list: ^1.0.1 copied to clipboard
A sliver with a pinned button tab bar over sections that may nest and mix row heights. The tab bar follows the scroll position.
import 'package:flutter/material.dart';
import 'package:sliver_tabbar_with_mixed_list/sliver_tabbar_with_mixed_list.dart';
void main() => runApp(const MyApp());
const double kHeaderHeight = 64;
const double kSubHeaderHeight = 44;
const double kDishHeight = 72;
const double kFeaturedHeight = 110;
class Course extends HeaderItem {
const Course({
required super.key,
required this.name,
required super.tabLabel,
super.children,
super.subSections,
}) : super(itemHeight: kHeaderHeight);
final String name;
}
class SubCourse extends SubheaderItem {
const SubCourse({required super.key, required this.name, super.children})
: super(itemHeight: kSubHeaderHeight);
final String name;
}
class Dish extends ChildItem {
const Dish({required super.key, required this.name, required this.price})
: super(itemHeight: kDishHeight);
final String name;
final String price;
}
/// A taller row, drawn by `variantChildBuilder`.
class Featured extends VariantChildItem {
const Featured({required super.key, required this.name})
: super(itemHeight: kFeaturedHeight);
final String name;
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'SliverTabBarWithMixedList Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
static const List<String> _courses = <String>[
'Starters',
'Soups',
'Mains',
'Grill',
'Desserts',
'Drinks',
];
final ScrollController _controller = ScrollController();
late final List<HeaderItem> _sections;
int _section = 0;
@override
void initState() {
super.initState();
// No offset arithmetic anywhere: the package sums the row heights itself.
_sections = <HeaderItem>[
for (int i = 0; i < _courses.length; i++)
Course(
key: ValueKey<int>(i),
name: _courses[i],
tabLabel: _courses[i],
children: <ChildItem>[
if (i == 0)
Featured(key: ValueKey<String>('f$i'), name: 'Chef pick'),
for (int j = 0; j < 3; j++)
Dish(
key: ValueKey<String>('$i-$j'),
name: '${_courses[i]} ${j + 1}',
price: '${(j + 1) * 9 + i} zl',
),
],
subSections: i.isEven
? <SubheaderItem>[
SubCourse(
key: ValueKey<String>('sub-$i'),
name: '${_courses[i]} — house selection',
children: <ChildItem>[
for (int j = 0; j < 2; j++)
Dish(
key: ValueKey<String>('$i-s-$j'),
name: 'House ${_courses[i]} ${j + 1}',
price: '${(j + 1) * 12} zl',
),
],
),
]
: null,
),
];
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final ColorScheme colors = Theme.of(context).colorScheme;
return Scaffold(
body: CustomScrollView(
controller: _controller,
slivers: <Widget>[
SliverAppBar(
pinned: true,
title: const Text('Menu'),
actions: <Widget>[
Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text('section $_section'),
),
),
],
),
SliverTabBarWithMixedList(
controller: _controller,
listItemHeight: kDishHeight,
sections: _sections,
onSectionChanged: (int index) => setState(() => _section = index),
tabBarPadding: const EdgeInsets.symmetric(vertical: 4),
headerBuilder: (BuildContext context, HeaderItem item) =>
ColoredBox(
color: colors.primaryContainer,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
(item as Course).name,
style: Theme.of(context).textTheme.titleLarge,
),
),
),
),
subHeaderBuilder: (BuildContext context, SubheaderItem item) =>
ColoredBox(
color: colors.secondaryContainer,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Align(
alignment: Alignment.centerLeft,
child: Text((item as SubCourse).name),
),
),
),
childBuilder: (BuildContext context, ChildItem item) {
final Dish dish = item as Dish;
return ListTile(
title: Text(dish.name),
trailing: Text(dish.price),
);
},
variantChildBuilder:
(BuildContext context, VariantChildItem item) => Card(
margin: const EdgeInsets.all(8),
color: colors.tertiaryContainer,
child: Center(child: Text((item as Featured).name)),
),
customFooterWidget: const Center(child: Text('end of the menu')),
),
],
),
);
}
}