Dynamic Bottom Sheet In Flutter

Mobile Apps Academy
4 min readFeb 22, 2024

In this article, We will explore how to implement a Dynamic Bottom Sheet in Flutter

Before proceeding, please consider subscribing to our YOUTUBE CHANNEL

It gives us a lot of motivation to produce high-quality content for you guys.

if you are interested in watching the video tutorial, Check out below.

Let's start by creating a StatefulWidget and name it MyDraggableSheet.


class MyDraggableSheet extends StatefulWidget {

@override
State<MyDraggableSheet> createState() => _MyDraggableSheetState();
}

class _MyDraggableSheetState extends State<MyDraggableSheet> {

@override
void dispose() {
super.dispose();
}

@override
Widget build(BuildContext context) {
return PlaceHolder();
}

}

We need a few variables to work with. Let's add them.

DraggableScrollableController is disposed of inside the dispose() method once the usage is completed.

class MyDraggableSheet extends StatefulWidget {
final Widget child;
const MyDraggableSheet({super.key, required this.child});

@override
State<MyDraggableSheet> createState() => _MyDraggableSheetState();
}

class _MyDraggableSheetState extends State<MyDraggableSheet> {
final sheet = GlobalKey();
final controller = DraggableScrollableController();

@override
void initState() {
super.initState();
controller.addListener(onChanged);
}


@override
void dispose() {
super.dispose();
controller.dispose();
}

DraggableScrollableSheet get getSheet =>
(sheet.currentWidget as DraggableScrollableSheet);

@override
Widget build(BuildContext context) {
return PlaceHolder;
}
}

Our bottom sheet will have a few states which we will call onChanged()

  1. collapse()
  2. anchor()
  3. expand()
  4. hide()
class MyDraggableSheet extends StatefulWidget {
final Widget child;
const MyDraggableSheet({super.key, required this.child});

@override
State<MyDraggableSheet> createState() => _MyDraggableSheetState();
}

class _MyDraggableSheetState extends State<MyDraggableSheet> {
final sheet = GlobalKey();
final controller = DraggableScrollableController();

@override
void initState() {
super.initState();
controller.addListener(onChanged);
}

void onChanged() {
final currentSize = controller.size;
if (currentSize <= 0.05) collapse();
}

void collapse() => animateSheet(getSheet.snapSizes!.first);

void anchor() => animateSheet(getSheet.snapSizes!.last);

void expand() => animateSheet(getSheet.maxChildSize);

void hide() => animateSheet(getSheet.minChildSize);

void animateSheet(double size) {
controller.animateTo(
size,
duration: const Duration(milliseconds: 50),
curve: Curves.easeInOut,
);
}

@override
void dispose() {
super.dispose();
controller.dispose();
}

DraggableScrollableSheet get getSheet =>
(sheet.currentWidget as DraggableScrollableSheet);

@override
Widget build(BuildContext context) {
return PlaceHolder();
}


SliverToBoxAdapter topButtonIndicator() {
return SliverToBoxAdapter(
child: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Container(
child: Center(
child: Wrap(children: <Widget>[
Container(
width: 100,
margin: const EdgeInsets.only(top: 10, bottom: 10),
height: 5,
decoration: const BoxDecoration(
color: Colors.black45,
shape: BoxShape.rectangle,
borderRadius: BorderRadius.all(Radius.circular(8.0)),
)),
]))),
])),
);
}
}

I’m using a LayoutBuilder which returns a DraggableScrollableSheet where we pass the widget value.

class MyDraggableSheet extends StatefulWidget {
final Widget child;
const MyDraggableSheet({super.key, required this.child});

@override
State<MyDraggableSheet> createState() => _MyDraggableSheetState();
}

class _MyDraggableSheetState extends State<MyDraggableSheet> {
final sheet = GlobalKey();
final controller = DraggableScrollableController();

@override
void initState() {
super.initState();
controller.addListener(onChanged);
}

void onChanged() {
final currentSize = controller.size;
if (currentSize <= 0.05) collapse();
}

void collapse() => animateSheet(getSheet.snapSizes!.first);

void anchor() => animateSheet(getSheet.snapSizes!.last);

void expand() => animateSheet(getSheet.maxChildSize);

void hide() => animateSheet(getSheet.minChildSize);

void animateSheet(double size) {
controller.animateTo(
size,
duration: const Duration(milliseconds: 50),
curve: Curves.easeInOut,
);
}

@override
void dispose() {
super.dispose();
controller.dispose();
}

DraggableScrollableSheet get getSheet =>
(sheet.currentWidget as DraggableScrollableSheet);

@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (context, constraints) {
return DraggableScrollableSheet(
key: sheet,
initialChildSize: 0.5,
maxChildSize: 0.95,
minChildSize: 0,
expand: true,
snap: true,
snapSizes: [
60 / constraints.maxHeight,
0.5,
],
controller: controller,
builder: (BuildContext context, ScrollController scrollController) {
return DecoratedBox(
decoration: const BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.yellow,
blurRadius: 10,
spreadRadius: 1,
offset: Offset(0, 1),
),
],
borderRadius: BorderRadius.only(
topLeft: Radius.circular(22),
topRight: Radius.circular(22),
),
),
child: CustomScrollView(
controller: scrollController,
slivers: [
topButtonIndicator(),
SliverToBoxAdapter(
child: widget.child,
),
],
),
);
},
);
});
}

SliverToBoxAdapter topButtonIndicator() {
return SliverToBoxAdapter(
child: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Container(
child: Center(
child: Wrap(children: <Widget>[
Container(
width: 100,
margin: const EdgeInsets.only(top: 10, bottom: 10),
height: 5,
decoration: const BoxDecoration(
color: Colors.black45,
shape: BoxShape.rectangle,
borderRadius: BorderRadius.all(Radius.circular(8.0)),
)),
]))),
])),
);
}
}

I have created a dummy UI for our bottom sheet. We pass this dummy UI through our main page.

class BottomSheetDummyUI extends StatelessWidget {
const BottomSheetDummyUI({super.key});

@override
Widget build(BuildContext context) {
return Container(
child: Container(
padding: EdgeInsets.only(left: 30, right: 30),
child: Column(
children: [
Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(15.0),
child: Container(
color: Colors.black12,
height: 100,
width: 100,
),
),
SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(15.0),
child: Container(
color: Colors.black12,
height: 20,
width: 240,
),
),
SizedBox(height: 5),
ClipRRect(
borderRadius: BorderRadius.circular(15.0),
child: Container(
color: Colors.black12,
height: 20,
width: 180,
),
),
SizedBox(height: 50),
],
)
],
),
SizedBox(height: 10),
],
)),
);
}
}

Below is how we use the sheet with a dummy UI.

class MyApp extends StatelessWidget {
const MyApp({super.key});

// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
backgroundColor: Colors.black,
body: MyDraggableSheet(
child: Column(
children: [
BottomSheetDummyUI(),
BottomSheetDummyUI(),
BottomSheetDummyUI(),
BottomSheetDummyUI(),
BottomSheetDummyUI(),
BottomSheetDummyUI(),
BottomSheetDummyUI(),
],
)),
),
);
}
}

That’s it. You should be able to see the sheet-like below

Once again Thanks for stopping by.
Do check out our YOUTUBE CHANNEL

Social Handles

Instagram : https://www.instagram.com/mobileappsacademy/

Twitter : https://twitter.com/MobileAppsAcdmy

LinkedIn : https://www.linkedin.com/company/mobile-apps-academy/

--

--

Mobile Apps Academy

Welcome to Mobile Apps Academy, your go-to channel for all things mobile app development!