I'm trying to implement a layout with a non-solid background (e.g. a gradient), while using a SliverAppBar with FlexibleSpaceBar. The goal is to make the SliverAppBar transparent, so the background remains visible behind it, including in the collapsed state. The problem is that, scrolled body is visible under the appBar.
You can check out a minimal reproducible example here: git
import 'package:flutter/material.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),
),
home: const Screen(),
);
}
}
class Screen extends StatefulWidget {
const Screen({super.key});
@override
State<Screen> createState() => _ScreenState();
}
class _ScreenState extends State<Screen> {
double _listTopPadding = kToolbarHeight;
@override
Widget build(BuildContext context) {
return Container(
// Add background gradient
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.amber, Colors.blue],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Scaffold(
//Make scaffold transparent to show gradient
backgroundColor: Colors.transparent,
body: SafeArea(
child: NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverAppBar(
backgroundColor: Colors
.transparent, //Make appbar transparent tho show gradient
forceMaterialTransparency: true,
pinned: true,
expandedHeight: 200,
collapsedHeight: kToolbarHeight,
flexibleSpace: FlexibleSpaceBar(
titlePadding: EdgeInsets.zero,
//Add some content
title: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text('Felxible space'),
Container(
height: 1,
width: double.infinity,
color: Colors.black,
),
],
),
),
),
],
body: Padding(
padding: EdgeInsets.only(top: _listTopPadding),
child: ListView.builder(
itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
width: double.infinity,
height: 75,
color: Colors.red,
),
),
),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => setState(() {
if (_listTopPadding > 0) {
_listTopPadding = 0;
} else {
_listTopPadding = kToolbarHeight;
}
}),
),
),
);
}
}
So far, the only workaround I’ve found is adding top padding to the body content - but in the real-world scenario, this padding between the AppBar and body is not acceptable. The background should seamlessly continue behind the AppBar, both expanded and collapsed.
Pretty much a bump of flutter-in-a-customscrollview-how-to-prevent-a-scrolled-widget-from-being-draw
...but with a full code sample and, I believe, more concrete details.
What I need:
Any ideas how to achieve this cleanly?
To resolve the issue, you can use a matching gradient background for the SliverAppBar’s flexibleSpace. Since NestedScrollView and CustomScrollView allow the body content to scroll underneath the app bar, a transparent SliverAppBar exposes this content.
The solution is to make the app bar's background opaque, but instead of a solid color, use the same gradient as your page background. This creates the illusion of a seamless background while hiding the scrolling list items. Typically, a CustomScrollView is cleaner than a NestedScrollView for a single scrolling body.
import 'package:flutter/material.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),
),
home: const Screen(),
);
}
}
class Screen extends StatelessWidget {
const Screen({super.key});
@override
Widget build(BuildContext context) {
// Use a root Container for the screen's background gradient.
return Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Colors.amber, Colors.blue],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Scaffold(
// Make the Scaffold's background transparent.
backgroundColor: Colors.transparent,
body: SafeArea(
// Use a CustomScrollView for a cleaner implementation.
child: CustomScrollView(
slivers: [
SliverAppBar(
//Keep the AppBar's own color transparent.
backgroundColor: Colors.transparent,
pinned: true,
expandedHeight: 200,
// THE FIX: Add a Container with the same gradient to the flexibleSpace.
// This Container acts as the app bar's background. It's not
// transparent, so it will hide the list content scrolling behind it.
// Because the gradient is identical to the page's background, the
// effect is seamless.
flexibleSpace: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Colors.amber, Colors.blue],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: FlexibleSpaceBar(
titlePadding: EdgeInsets.zero,
title: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const Text('Flexible space'),
Container(
height: 1,
width: double.infinity,
color: Colors.black,
),
],
),
),
),
),
// Use SliverList for the body content. No top padding is needed.
SliverList.builder(
itemCount: 20, // Added for reproducibility
itemBuilder: (context, index) => Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
width: double.infinity,
height: 75,
color: Colors.red,
),
),
),
],
),
),
),
);
}
}