When diving into the domain of designing dynamic app layouts in Flutter, it's hard to overlook the innovative concept of Flutter Slivers. Slivers in Flutter are box model widgets that provide a flexible system to implement a custom scroll model in your Flutter applications. The brilliance of Slivers lies in their ability to change their size and shape as the user scrolls. For instance, an app bar can stretch, shrink, and even disappear as the user scrolls, providing a rich scrolling experience.
To be more precise, a sliver widget is a portion of a scrollable area. You can think of Flutter Slivers as a linear array of scrollable widgets with varying dimensions. However, the Sliver Flutter experience is anything but linear. It opens up a magical world within your apps where the typical laws of a scrollable area no longer apply. Your app bar's overall height can morph, transform, and adapt in real time.
As you step into the world of Slivers in Flutter, you'll encounter a variety of Sliver widgets, each with its unique set of capabilities and applications. Let's explore a few common types:
The Flutter SliverList is a widget that arranges its box children in a linear fashion along the main axis. SliverList, essentially, is an amalgamation of a sliver and an ordinary list. It allows the children of lists to load lazily (i.e., as they become visible).
Below is a quick example of how to render a list of widgets with SliverList.
1 void main() { 2 runApp( 3 MaterialApp( 4 home: MyApp(), 5 ), 6 ); 7 } 8 9 class MyApp extends StatelessWidget { 10 @override 11 Widget build(BuildContext context) { 12 return CustomScrollView( 13 slivers: <Widget>[ 14 SliverList( 15 delegate: SliverChildListDelegate( 16 List.generate( 17 25, 18 (int index) => ListTile(title: Text('Item #$index')), 19 ), 20 ), 21 ), 22 ], 23 ); 24 } 25 }
The above code creates a Flutter SliverList with 25 elements each displaying a widget as a child. You create your list in the delegate parameter, with each ListTile widget representing a single item on the list.
In case you need more than a linear layout, meet SliverGrid. Imagine a horizontal axis interspersed with your vertical scrollable area, and you get a grid layout. SliverGrid gives you a two-dimensional arrangement of your children's widgets. The maximum width along the cross axis (often the vertical axis for English language apps) is defined through an explicit GridDelegate parameter.
Here's a sample code snippet for a simple SliverGrid:
1 void main() => runApp(MyApp()); 2 3 class MyApp extends StatelessWidget { 4 @override 5 Widget build(BuildContext context) { 6 return MaterialApp( 7 home: Scaffold( 8 body: CustomScrollView( 9 slivers: [ 10 SliverGrid( 11 gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( 12 maxCrossAxisExtent: 200.0, 13 mainAxisSpacing: 10.0, 14 crossAxisSpacing: 10.0, 15 childAspectRatio: 4.0, 16 ), 17 delegate: SliverChildBuilderDelegate( 18 (BuildContext context, int index) { 19 return Container( 20 alignment: Alignment.center, 21 color: Colors.teal[100 * (index % 9)], 22 child: Text('grid item $index'), 23 ); 24 }, 25 childCount: 20, 26 ), 27 ), 28 ], 29 ), 30 ), 31 ); 32 } 33 }
This code renders a grid with a maximum width (maxCrossAxisExtent) of 200 on the screen for each grid item.
Another sliver widget you'll find fascinating is the SliverFillViewport. This widget sizes its children to fill in the remaining main axis extent, given one or multiple children with a maximum width. It's quite handy when you want to ensure that a widget, such as a column widget, takes up at least the screen's total main axis extent.
Here's a simple usage of SliverFillViewport in Flutter:
1 void main() { 2 runApp(MyApp()); 3 } 4 5 class MyApp extends StatelessWidget { 6 @override 7 Widget build(BuildContext context) { 8 return MaterialApp( 9 home: CustomScrollView( 10 slivers: [ 11 SliverFillViewport( 12 delegate: SliverChildListDelegate( 13 [ 14 Container(color: Colors.red, height: 150.0), 15 Container(color: Colors.purple, height: 150.0), 16 Container(color: Colors.green, height: 150.0), 17 ] 18 ), 19 viewportFraction: 0.3 20 ), 21 ] 22 ) 23 ); 24 } 25 }
The SliverFillViewport widget uses a property viewportFraction to define the fraction of the viewport to fill. In the above code, each widget will fill a third (0.3) of the screen.
These are just a few examples from the entire family of Flutter Slivers. Experiment with these widgets to create stellar designs in your Flutter application.
Knowing the basics of using Slivers is great, but to make the most out of your UI designs, understanding some core concepts related to Slivers is beneficial. This section takes you through a few of them:
A SliverAppBar in Flutter is like a blessing in disguise when you want a flexible app bar. As the user scrolls, the app bar could shrink its height until it becomes a classic Material Design App Bar. The best part? It can even disappear completely as the user scrolls down.
Here's an example of how you'd incorporate it into your application:
1 void main() { 2 runApp(MyApp()); 3 } 4 5 class MyApp extends StatelessWidget { 6 @override 7 Widget build(BuildContext context) { 8 return MaterialApp( 9 home: CustomScrollView( 10 slivers: <Widget>[ 11 SliverAppBar( 12 expandedHeight: 150.0, 13 floating: false, 14 pinned: true, 15 flexibleSpace: FlexibleSpaceBar( 16 title: Text("Flutter SliverAppBar"), 17 ), 18 ), 19 ] 20 ), 21 ); 22 } 23 }
In the above code, SliverAppBar allows you to create an app bar that can change and adapt as the user scrolls. The SliverAppBar has a property expandedHeight to specify the maximum height it should have. The pinned attribute retains the app bar's presence even when it's scrolled off screen while the floating property allows it to reappear as soon as the user scrolls towards the app start.
Another handy element to use from the Flutter Slivers toolbox is SliverPersistentHeader. This widget remains visible at the top of the ScrollView. As the user scrolls tied up with the SliverPersistentHeader, it can alter its size, providing an impressive scroll effect.
In the code sample you are about to see, we define a custom SliverPersistentHeaderDelegate to create the header:
1 class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { 2 _SliverAppBarDelegate({ 3 @required this.minHeight, 4 @required this.maxHeight, 5 @required this.child, 6 }); 7 8 final double minHeight; 9 final double maxHeight; 10 final Widget child; 11 12 @override 13 double get minExtent => minHeight; 14 15 @override 16 double get maxExtent => max(maxHeight, minHeight); 17 18 @override 19 Widget build( 20 BuildContext context, 21 double shrinkOffset, 22 bool overlapsContent 23 ) { 24 return SizedBox.expand(child: child); 25 } 26 27 @override 28 bool shouldRebuild(_SliverAppBarDelegate oldDelegate) { 29 return maxHeight != oldDelegate.maxHeight || 30 minHeight != oldDelegate.minHeight || 31 child != oldDelegate.child; 32 } 33 } 34 35 void main() => runApp(MyApp()); 36 37 class MyApp extends StatelessWidget { 38 @override 39 Widget build(BuildContext context) { 40 return MaterialApp( 41 home: Scaffold( 42 body: CustomScrollView( 43 slivers: [ 44 SliverPersistentHeader( 45 delegate: _SliverAppBarDelegate( 46 minHeight: 60.0, 47 maxHeight: 200.0, 48 child: Container( 49 color: Colors.green, 50 child: Center( 51 child: Text('SliverPersistentHeader') 52 ), 53 ) 54 ) 55 ), 56 ], 57 ), 58 ), 59 ); 60 } 61 }
This is a simplified version of a SliverPersistentHeader. You can adapt the delegate constructor to receive any type of Widget and further customize the appearance on your application.
For scenarios where you need a scrollable list of items with the same extent along the scroll axis, SliverFixedExtentList comes to the rescue. It can be seen as a more efficient version of SliverList in these cases, since it doesn't need to calculate the extent of each item.
Here's how to implement it:
1 void main() => runApp(MyApp()); 2 3 class MyApp extends StatelessWidget { 4 @override 5 Widget build(BuildContext context) { 6 return MaterialApp( 7 home: CustomScrollView( 8 slivers:[ 9 <Widget>[ 10 SliverFixedExtentList( 11 itemExtent: 100.0, 12 delegate: SliverChildListDelegate( 13 List.generate( 14 15, 15 (int index) => ListTile( 16 title: Text('Item $index'), 17 tileColor: index % 2 == 0 ? Colors.white : Colors.grey[300], 18 ) 19 ) 20 ) 21 ), 22 ], 23 ), 24 ); 25 } 26 }
The itemExtent property in SliverFixedExtentList defines the main axis extent. In this code snippet, we've set it as 100.0, hence each tile in the list will be 100.0 units in length.
Slivers are what separate Flutter's scroll model from the rest, giving developers unprecedented control over their scroll views. Armed with these concepts, you can take your app layouts to exciting new levels of user interaction and customization.
One of the most remarkable applications of slivers in Flutter is in creating custom scroll models. Flutter provides exceptional support for this through a specific widget known as Flutter CustomScrollView.
A CustomScrollView in Flutter is essentially a ScrollView with a few modifications. Unlike the typical scroll view, which includes a single box widget like ListView or GridView, CustomScrollView can contain a sequence of slivers, piecing together to build a scrollable area tailored to your business needs.
Let's look at an example to see how it works.
1 void main() { 2 runApp(MyApp()); 3 } 4 5 class MyApp extends StatelessWidget { 6 @override 7 Widget build(BuildContext context) { 8 return MaterialApp( 9 home: CustomScrollView( 10 slivers: <Widget>[ 11 SliverAppBar( 12 expandedHeight: 250.0, 13 flexibleSpace: const FlexibleSpaceBar( 14 title: Text('Flutter Custom Scroll View'), 15 ), 16 actions: <Widget>[ 17 IconButton( 18 icon: const Icon(Icons.add_circle), 19 tooltip: 'Add new entry', 20 onPressed: () {}, 21 ), 22 ] 23 ), 24 SliverList( 25 delegate: SliverChildBuilderDelegate( 26 (BuildContext context, int index) { 27 return ListTile( 28 title: Text('Item #$index'), 29 ); 30 }, 31 childCount: 1000 32 ), 33 ), 34 ], 35 ), 36 ); 37 } 38 }
In this example, we first create a SliverAppBar and then a SliverList widget. Both of these widgets are passed as a list to the slivers parameter in CustomScrollView. Thus, instead of merely using a single box widget, we integrated multiple slivers smoothly to compile into a highly adaptive and custom scrollable area.
After understanding the basics of CustomScrollView, let's see it at work handling multiple types of slivers.
1 void main() { 2 runApp(const MyApp()); 3 } 4 5 class MyApp extends StatelessWidget { 6 const MyApp({Key? key}) : super(key: key); // using default constructor 7 8 @override 9 Widget build(BuildContext context) { 10 return MaterialApp( 11 home: CustomScrollView( 12 slivers: [ 13 SliverAppBar( 14 expandedHeight: 150.0, 15 title: const Text('SliverAppBar with Flutter CustomScrollView'), 16 pinned: true, 17 ), 18 ], 19 ), 20 ); 21 } 22 }
In this example, a SliverAppBar is integrated within the CustomScrollView. It gives an app bar that collapses as the user scrolls down the page.
1 void main() { 2 runApp(MyApp()); 3 } 4 5 class MyApp extends StatelessWidget { 6 @override 7 Widget build(BuildContext context) { 8 return MaterialApp( 9 home: CustomScrollView( 10 slivers: <Widget>[ 11 SliverAppBar( 12 title: Text('Sliver Grid and List within CustomScrollView'), 13 expandedHeight: 200.0, 14 flexibleSpace: FlexibleSpaceBar( 15 background: Image.network('https://url-to-your-image.jpg'), 16 ), 17 ), 18 SliverGrid( 19 delegate: SliverChildBuilderDelegate( 20 (BuildContext context, int index) { 21 return Container( 22 alignment: Alignment.center, 23 color: Colors.teal[100 * (index % 9)], 24 child: Text('Grid Item $index'), 25 ); 26 }, 27 childCount: 20, 28 ), 29 gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( 30 maxCrossAxisExtent: 200.0, 31 mainAxisSpacing: 10.0, 32 crossAxisSpacing: 10.0, 33 childAspectRatio: 4.0, 34 ), 35 ), 36 SliverFixedExtentList( 37 itemExtent: 50.0, 38 delegate: SliverChildBuilderDelegate( 39 (BuildContext context, int index) { 40 return Container( 41 alignment: Alignment.center, 42 color: Colors.lightBlue[100 * (index %9)], 43 child: Text('List Item $index'), 44 ); 45 }, 46 childCount: 20, 47 ), 48 ), 49 ], 50 ), 51 ); 52 } 53 }
The above code features the use of both SliverGrid and SliverList together in a CustomScrollView. It exhibits how you can bunch multiple slivers together and build a cohesive and customized, scrollable region in your Flutter application.
What's clear so far is that by leveraging the power of slivers and custom scroll views in clever ways, you can create fascinating and engaging user interfaces. More importantly, it encourages the creation of mobile app layouts that are catered specifically to the requirements of your brand and your audience.
Now that we're familiar with several of the theory-guided examples on how to integrate and use slivers, let's switch gears and examine how they're applied in actual scenarios, laying the foundation for intricate UI solutions.
By now we've discussed how SliverList can be your go-to sliver widget for generating a linear array of children. Here, we’ll create a simple SliverList that presents a list of planets.
1 void main() { 2 runApp(MyApp()); 3 } 4 5 class MyApp extends StatelessWidget { 6 @override 7 Widget build(BuildContext context) { 8 return MaterialApp( 9 home: Scaffold( 10 body: CustomScrollView( 11 slivers: [ 12 SliverList( 13 delegate: SliverChildListDelegate( 14 List.generate( 15 8, 16 (int index) { 17 return Container( 18 height: 100, 19 color: index % 2 == 0 ? Colors.white : Colors.grey[300], 20 child: Center( 21 child: Text( 22 'Planet ${index + 1}', 23 style: TextStyle(fontSize: 24), 24 ), 25 ), 26 ); 27 }, 28 ), 29 ), 30 ), 31 ], 32 ), 33 ), 34 ); 35 } 36 }
In the above code, we've created a SliverList. Here, we use SliverChildListDelegate to create an explicit list of eight container widgets, each corresponding to a planet. Each container is either white or grey, depending on whether its index is odd or even to provide some variety in our list.
Below is an example of SliverGrid where we'll display a grid of planet images.
1 void main() { 2 runApp(MyApp()); 3 } 4 5 class MyApp extends StatelessWidget { 6 @override 7 Widget build(BuildContext context) { 8 return MaterialApp( 9 home: Scaffold( 10 body: CustomScrollView( 11 slivers: [ 12 SliverGrid( 13 delegate: SliverChildBuilderDelegate( 14 (BuildContext context, int index) { 15 return Container( 16 color: Colors.teal[100 * (index % 9)], 17 child: Image.network('https://path-to-your-image-$index.jpg'), 18 ); 19 }, 20 childCount: 12, 21 ), 22 gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 23 crossAxisCount: 3, 24 mainAxisSpacing: 2, 25 crossAxisSpacing: 2, 26 ), 27 ), 28 ], 29 ), 30 ), 31 ); 32 } 33 }
The above code will display a grid of 12 images arranged in 3 columns. It makes use of the SliverGrid widget with a SliverChildBuilderDelegate to generate 12 children widgets each containing an image.
1 void main() { 2 runApp(MyApp()); 3 } 4 5 class MyApp extends StatelessWidget { 6 @override 7 Widget build(BuildContext context) { 8 return MaterialApp( 9 home: Scaffold( 10 body: CustomScrollView( 11 slivers: [ 12 SliverAppBar( 13 title: Text('Planets'), 14 backgroundColor: Colors.teal, 15 expandedHeight: 250.0, 16 flexibleSpace: FlexibleSpaceBar( 17 background: Image.network('https://path-to-your-image.jpg', 18 fit: BoxFit.cover), 19 ), 20 ), 21 SliverList( 22 delegate: SliverChildBuilderDelegate( 23 (BuildContext context, int index) { 24 return Container( 25 height: 50, 26 color: index % 2 == 0 ? Colors.white : Colors.grey[300], 27 child: Center( 28 child: Text( 29 'Planet ${index + 1}', 30 style: TextStyle(fontSize: 16), 31 ), 32 ), 33 ); 34 }, 35 childCount: 10, 36 ), 37 ), 38 ], 39 ), 40 ), 41 ); 42 } 43 }
This example demonstrates the implementation of SliverAppBar in a CustomScrollView, presenting an app bar that changes its size as the user scrolls, to maintain an engaging and responsive UI.
It's time to realize the potential of more than one sliver at a time, thus truly revolutionizing the scrolling experience. In this block of code, we combine the different types of slivers we've learned about to create a more complex and interactive UI.
1 void main() { 2 runApp(MyApp()); 3 } 4 5 class MyApp extends StatelessWidget { 6 @override 7 Widget build(BuildContext context) { 8 return MaterialApp( 9 home: CustomScrollView( 10 slivers: [ 11 SliverAppBar( 12 title: Text('The Solar System'), 13 backgroundColor: Colors.teal, 14 expandedHeight: 200.0, 15 flexibleSpace: FlexibleSpaceBar( 16 background: Image.network('https://path-to-your-image.jpg', 17 fit: BoxFit.cover), 18 ), 19 ), 20 SliverGrid( 21 delegate: SliverChildBuilderDelegate( 22 (BuildContext context, int index) { 23 return Container( 24 color: Colors.teal[100 * (index % 9)], 25 child: Center( 26 child: Text( 27 'Planet ${index + 1}', 28 style: TextStyle(fontSize: 16, color: Colors.white), 29 ), 30 ), 31 ); 32 }, 33 childCount: 8, 34 ), 35 gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 36 crossAxisCount: 2, 37 mainAxisSpacing: 10, 38 crossAxisSpacing: 10, 39 childAspectRatio: 4.0, 40 ), 41 ), 42 SliverList( 43 delegate: SliverChildBuilderDelegate( 44 (BuildContext context, int index) { 45 return Container( 46 height: 100, 47 color: index % 2 == 0 ? Colors.white : Colors.grey[300], 48 child: ListTile( 49 leading: CircleAvatar( 50 backgroundColor: Colors.indigo, 51 ), 52 title: Text('Moon ${index + 1}'), 53 ), 54 ); 55 }, 56 childCount: 3, 57 ), 58 ), 59 ], 60 ), 61 ); 62 } 63 }
In this example, we begin with our SliverAppBar, showcasing an image of the cosmos. Following this, we have a SliverGrid that displays information about different planets, each in a colored box that aligns perfectly with the space theme. Finally, we have a SliverList that presents details about the moons.
1void main() { 2 runApp(MyApp()); 3 } 4 5 class MyApp extends StatelessWidget { 6 @override 7 Widget build(BuildContext context) { 8 return MaterialApp( 9 home: CustomScrollView( 10 slivers: [ 11 SliverAppBar( 12 title: Text('Nested Slivers'), 13 backgroundColor: Colors.teal, 14 expandedHeight: 200.0, 15 flexibleSpace: FlexibleSpaceBar( 16 background: Image.network('https://path-to-your-image.jpg', 17 fit: BoxFit.cover), 18 ), 19 ), 20 SliverGrid( 21 delegate: SliverChildBuilderDelegate( 22 (BuildContext context, int index) { 23 return Container( 24 color: Colors.teal[100 * (index % 9)], 25 child: Center( 26 child: Text( 27 'Grid Item ${index + 1}', 28 style: TextStyle(fontSize: 16, color: Colors.white), 29 ), 30 ), 31 ); 32 }, 33 childCount: 4, 34 ), 35 gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( 36 crossAxisCount: 2, 37 mainAxisSpacing: 10, 38 crossAxisSpacing: 10, 39 childAspectRatio: 4.0, 40 ), 41 ), 42 SliverToBoxAdapter( 43 child: SizedBox( 44 height: 200.0, 45 child: ListView.builder( 46 scrollDirection: Axis.horizontal, 47 itemCount: 10, 48 itemExtent: 160.0, 49 itemBuilder: (BuildContext context, int index) { 50 return Container(color: Colors.teal[100 * (index % 9)]); 51 }, 52 ), 53 ), 54 ), 55 SliverList( 56 delegate: SliverChildBuilderDelegate( 57 (BuildContext context, int index) { 58 return Container( 59 height: 80, 60 color: index % 2 == 0 ? Colors.white : Colors.grey[300], 61 child: ListTile( 62 leading: CircleAvatar( 63 backgroundColor: Colors 64 ), 65 title: Text('Moon ${index + 1}'), 66 ), 67 ); 68 }, 69 childCount: 2, 70 ), 71 ), 72 ], 73 ), 74 ); 75 } 76 }
In this nested sliver example, we encase a horizontal ListView.builder inside a SizedBox, which in turn is positioned inside a SliverToBoxAdapter widget. This way, the inner ListView is still visible and can scroll horizontally even while the outer custom scroll view scrolls vertically.
Working with Slivers can be highly rewarding and equally challenging. Here are a few tips and approaches to tackle common hurdles you might encounter.
While slivers offer great potential, they also come with their complexities. Here are some troubleshooting tips for common challenges:
Now that we have examined the fundamental applications and practices of using Slivers, we can proceed to take a look into more advanced features and analysis of Slivers in action.
Despite their complexity, slivers provide enormous potential for creating dynamic, flexible user interfaces. You can further enhance the user experience by leveraging some of the advanced features offered by slivers.
While Slivers can substantially empower your applications, they might at times throw puzzling errors. Here's an important approach to handle and debug them:
Understanding and Resolving Common Sliver Bugs: Several errors can occur due to incorrect nesting of Slivers or infraction of certain Slivers' rules. To fix this, ensure that widget return types in delegates match the required type and inspect the tree hierarchy for any Sliver-related naming conflict. An effective way to catch and resolve these errors is by using Flutter's debug mode.
Slivers provide you with an incredible amount of control over your app's scrollable regions, much more than you'd get with traditional box widgets. Whether it's a dynamically expanding app bar, grids and lists that load items as they come into view, or a combination of varied scrolling effects - all are achievable through the power of Flutter Slivers.
We've seen that with a mere flip in perspective, one can harness the flexibility and power of Slivers to create highly dynamic and immersive scrolling experiences. From understanding the core concepts of Slivers and implementing CustomScrollView to executing real-world examples and learning some efficient practices, we’ve paved a long way.
Flutter Slivers, in truth, are an amazing set of tools that every Flutter developer must get used to for creating innovative layouts and interfaces. The journey of mastering Slivers may seem ridden with obstacles, but as rightly said, the harder the conflict, the more glorious the triumph.
Flutter, with its rich set of features like Slivers, continues to stand as one of the most potent frameworks to build visually stunning and functionally rich applications. As developers, our endeavor should be to understand these features and employ them in our projects to create applications that enrich the end-user experience.
And that concludes our in-depth exploration into the dynamic world of slivers in Flutter. Journeying through the basics and diving into more advanced concepts, I hope you gained valuable insights. There's a lot more to discover and practice, so keep flapping those Flutter wings. Happy developing!
Tired of manually designing screens, coding on weekends, and technical debt? Let DhiWise handle it for you!
You can build an e-commerce store, healthcare app, portfolio, blogging website, social media or admin panel right away. Use our library of 40+ pre-built free templates to create your first application using DhiWise.