Thank you for this package. It works quite well, but I found a Use-Case which doesn't seem to work.
Thank you for looking at this, if you make this work it would be spectacular because it seems the "godfather" of indexed lists also has its problems when it comes to NestedScrollViews; google/flutter.widgets#32. Thank you.
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:sliver_tools/sliver_tools.dart';
import 'package:super_sliver_list/super_sliver_list.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
late final ScrollController verticalController;
late final ScrollController horizontalController;
late final ListController listController;
@override
void initState() {
super.initState();
verticalController = ScrollController();
horizontalController = ScrollController();
listController = ListController();
listController.addListener(() {
log('ListController visibleRange: ${listController.visibleRange?.$1} - ${listController.visibleRange?.$2}');
});
}
@override
void dispose() {
verticalController.dispose();
horizontalController.dispose();
listController.dispose();
super.dispose();
}
final List<String> tabs = <String>['Products', 'Checkout'];
final Map<String, List<String>> categoriesProducts = {
'Drinks': [
'Coke',
'Pepsi',
'Fanta',
'Sprite',
'Mountain Dew',
'Dr. Pepper',
'7UP'
],
'Snacks': [
'Lays',
'Doritos',
'Cheetos',
'Pringles',
'Ruffles',
'Tostitos',
'Fritos'
],
'Chocolates': [
'Snickers',
'Mars',
'Twix',
'KitKat',
'Hershey',
'Cadbury',
'Milky Way'
],
'Ice Creams': [
'Vanilla',
'Chocolate',
'Strawberry',
'Mint',
'Butter Pecan',
'Cookies & Cream',
'Rocky Road'
],
'Candies': [
'Skittles',
'M&M',
'Jelly Beans',
'Gummy Bears',
'Sour Patch Kids',
'Swedish Fish',
'Twizzlers'
],
'Cookies': [
'Oreo',
'Chips Ahoy',
'Nutter Butter',
'Milano',
'Famous Amos',
'Keebler',
'Lorna Doone'
],
'Chips': [
'Ruffles',
'Lays',
'Doritos',
'Cheetos',
'Pringles',
'Tostitos',
'Fritos'
],
'Biscuits': [
'Digestive',
'Marie',
'Oreo',
'Parle-G',
'Good Day',
'Hide & Seek',
'Britannia'
],
'Cakes': [
'Chocolate',
'Vanilla',
'Strawberry',
'Red Velvet',
'Carrot',
'Cheese',
'Pound'
],
};
void verticalScrollToIndex(int index) {
listController.animateToItem(
index: index,
scrollController: verticalController,
alignment: 0.5,
duration: (e) => const Duration(milliseconds: 500),
curve: (e) => Curves.easeInOut,
);
}
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: tabs.length, // This is the number of tabs.
child: Scaffold(
body: SafeArea(
child: NestedScrollView(
controller: verticalController,
headerSliverBuilder:
(BuildContext context, bool innerBoxIsScrolled) {
// These are the slivers that show up in the "outer" scroll view.
return <Widget>[
SliverOverlapAbsorber(
// This widget takes the overlapping behavior of the SliverAppBar,
// and redirects it to the SliverOverlapInjector below. If it is
// missing, then it is possible for the nested "inner" scroll view
// below to end up under the SliverAppBar even when the inner
// scroll view thinks it has not been scrolled.
// This is not necessary if the "headerSliverBuilder" only builds
// widgets that do not overlap the next sliver.
handle:
NestedScrollView.sliverOverlapAbsorberHandleFor(context),
sliver: SliverAppBar(
title: const Text(
'Restaurant'), // This is the title in the app bar.
pinned: false,
expandedHeight: 150.0,
collapsedHeight: 80,
forceElevated: innerBoxIsScrolled,
),
),
];
},
body: Column(
children: [
TabBar(
// These are the widgets to put in each tab in the tab bar.
tabs: tabs.map((String name) => Tab(text: name)).toList(),
),
Expanded(
child: TabBarView(
// These are the contents of the tab views, below the tabs.
children: [
SafeArea(
top: false,
bottom: false,
child: Builder(
builder: (BuildContext context) {
return CustomScrollView(
key: PageStorageKey<String>('Products'),
slivers: <Widget>[
SliverOverlapInjector(
// This is the flip side of the SliverOverlapAbsorber
// above.
handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(
context),
),
SliverPinnedHeader(
child: Container(
color:
Theme.of(context).colorScheme.surface,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: horizontalController,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
...categoriesProducts.keys.map(
(category) => Padding(
padding:
const EdgeInsets.only(
right: 4.0),
child: TextButton(
onPressed: () {
verticalScrollToIndex(
categoriesProducts
.keys
.toList()
.indexOf(
category));
},
child: Text(category),
),
),
)
],
),
),
),
),
),
SuperSliverList(
listController: listController,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
final category = categoriesProducts.keys
.elementAt(index);
final List<String> products =
categoriesProducts[category] ?? [];
return Card(
elevation: 1,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding:
const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Text(category,
style: Theme.of(context)
.textTheme
.headlineSmall),
],
),
),
...products.map((String product) {
return ListTile(
title: Text(product),
);
}).toList(),
],
),
);
},
childCount:
categoriesProducts.keys.length),
),
],
);
},
),
),
SafeArea(
top: false,
bottom: false,
child: Builder(
builder: (BuildContext context) {
return CustomScrollView(
key: PageStorageKey<String>('Checkout'),
slivers: <Widget>[
SliverOverlapInjector(
// This is the flip side of the SliverOverlapAbsorber
// above.
handle: NestedScrollView
.sliverOverlapAbsorberHandleFor(
context),
),
SliverPinnedHeader(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
children: [
ElevatedButton(
child: Text('Buy'),
onPressed: () {
// ...
})
],
),
)),
SliverPadding(
padding: const EdgeInsets.all(8.0),
sliver: SuperSliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ListTile(
title: Text(
'Product ' + index.toString()));
}, childCount: 30),
),
),
],
);
},
),
)
]),
),
],
),
),
),
),
);
}
}