How to make reusable modal bottom sheet - flutter

usually I use showModalBottomSheet for each view to call a ModalBottomSheet with the same content on it. I just want to make it simple as I can call the class of reusable modal bottomsheet.
_moreModalBottomSheet(context) {
Size size = MediaQuery.of(context).size;
showModalBottomSheet(
isScrollControlled: true,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40.0),
),
context: context,
builder: (BuildContext bc) {
return Container(
height: size.height * 0.5,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topRight: Radius.circular(40.0),
topLeft: Radius.circular(40.0),
),
),
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
child: ListView(
physics: ClampingScrollPhysics(),
children: [
//content of modal bottomsheet
],
),
),
);
});
}
for example, I use button to show modal bottom sheet
ElevatedButton(onPressed: _moreModalBottomSheet(context), child: Text('show modal bottom sheet'))
I want to make _moreModalBottomSheet() as a class so it reusable.
on this answer its only a reusable a layout. But, what I trying to achieve is make a custom class ModalBottomSheet. So I can call ModalBottomSheet in other class only like ModalBottomSheet() not showModalBottomSheet that return ModalBottomSheet . It's that possible?

You just need to extract it to a new class like:
class ModalBottomSheet {
static void _moreModalBottomSheet(context) {
Size size = MediaQuery.of(context).size;
showModalBottomSheet(
isScrollControlled: true,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40.0),
),
context: context,
builder: (BuildContext bc) {
return Container(
height: size.height * 0.5,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topRight: Radius.circular(40.0),
topLeft: Radius.circular(40.0),
),
),
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
child: ListView(
physics: ClampingScrollPhysics(),
children: [
//content of modal bottomsheet
],
),
),
);
});
}
}
Now you can call it everywhere like:
class Test extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Center(
child: ElevatedButton(
onPressed: () =>
ModalBottomSheet._moreModalBottomSheet(context),
child: Text('open modal'),
),
),
),
);
}
}

Open bottom sheet like this
InkWell(
onTap: () {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (context) {
return ModalBottomSheet(
);
});
})
Stateful bottom sheet
class ModalBottomSheet extends StatefulWidget {
#override
_ModalBottomSheetState createState() => _ModalBottomSheetState();
}
class _ModalBottomSheetState extends State<ModalBottomSheet>
with SingleTickerProviderStateMixin {
#override
Widget build(BuildContext context) {
double keyboardHeight = MediaQuery.of(context).viewInsets.bottom;
// TODO: implement build
return Wrap(
children: <Widget>[
Container(
margin:
EdgeInsets.only(left: 10.0, right: 10.0, top: 15.0, bottom: 15.0),
child: Column(
children: <Widget>[
Widgets(),
]
)
)
],
);
}
}

Related

How can I add a programmatic scroll down to a list view builder which is inside a Modal Bottom Sheet in Flutter?

I am using a modal bottom sheet to display a list of challenges the user has faced. When the user clicks on an icon : the bottom sheet comes up and the list builds. All this is fine.
I am using a scrollController on the main screen for another list. When the user adds an item, I call _scrollDown(), a method I created to get an automatic scroll down (on the main screen) and this works fine.
I would like the same behavior in the bottom sheet. The problem is that I don't know where I can call my _scrollDown() method. The user clicks on "My Challenges", then the bottom sheet comes up and the list builds.... then it should scroll down... but I don't see where in the code I can add the method...
Here is the modal sheet code : (the gesture detector is on the icon used to get the list of challenges)
GestureDetector(
onTap: () {
showModalBottomSheet<void>(
isScrollControlled: true,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20.0),
topRight: Radius.circular(20.0),
),
),
context: context,
builder: (BuildContext context) {
return Container(
height: MediaQuery.of(context).size.height - 80,
child: SingleChildScrollView(
padding: EdgeInsets.only(
bottom:
MediaQuery.of(context).viewInsets.bottom),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
alignment: Alignment.topRight,
child: IconButton(
icon: Icon(
Icons.cancel,
color: Colors.red[900],
size: uD.largeFont3,
),
onPressed: () {
Navigator.pop(context);
}),
),
Container(
alignment: Alignment.topCenter,
margin: const EdgeInsets.only(bottom: 20),
child: Text(
uD.selectedIoLanguage == Language.french
? 'Mes défis'.toUpperCase()
: 'My Challenges'.toUpperCase(),
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.indigo[900],
fontSize: uD.largeFont3,
),
),
),
Container(
decoration: BoxDecoration(
border: Border(
top: BorderSide(
color: Colors.orange[200]!,
width: 3,
)),
gradient: LinearGradient(
colors: [
Colors.orange[200]!,
Colors.orange[50]!,
Colors.orange[100]!,
Colors.orange[200]!
],
begin: Alignment.topLeft,
end: Alignment.bottomLeft,
stops: [0, 0.2, 0.5, 0.8])),
height:
MediaQuery.of(context).size.height - 163,
child: uD.defis.length > 0
? ListView.builder(
controller: _scrollController,
cacheExtent: 2000,
padding: const EdgeInsets.all(20),
itemCount: uD.defis.length,
itemBuilder: (context, index) {
return MyDefisCard(
index: index,
score: uD.defis[index].score,
date: uD.defis[index].date,
time: uD.defis[index].time,
Move your bottom sheet builder result into a StatefulWidget, let's say it's called BottomSheetContent
showModalBottomSheet<void>(
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20.0),
topRight: Radius.circular(20.0),
),
),
context: context,
builder: (BuildContext context) => BottomSheetContent(),
);
Inside the new widget, create a new ScrollController (note that in your code you are using the same ScrollController from the list in the previous route)
class _BottomSheetContentState extends State<BottomSheetContent> {
ScrollController _scrollController = ScrollController();
#override
void initState() {
super.initState();
WidgetsBinding.instance!.addPostFrameCallback((_) {
// _scrollController.animateTo()
// same logic as in _scrollDown
});
}
#override
Widget build(BuildContext context) {
// return bottom sheet content
}
}

Flutter State won't update

I'm trying to create a listview of cards whose images get displayed in the listview only if the card is selected. The selection widget is the PopupSubscription() where I'm choosing which cards (SubscriptionCard) to display by setting the bool of that particular image to be true. But when the selections are applied, the selected images don't appear immediately even after doing setState().
However, they do appear when I switch tabs and return the screen again. What should I do in order to change the state of an object that's not in my current state class? I tried using Provider but it left me confused as to what I'm supposed to do.
This is the SubscriptionCard where the bool is set on tapping it:
return InkWell(
radius: 1.0,
borderRadius: BorderRadius.circular(8.0),
highlightColor: buttonBackground,
onTap: () {
setState(() {
widget.currentSubscription.selected = !widget.currentSubscription.selected;
});
},
child: Card(
elevation: 1.0,
borderOnForeground: true,
shape: widget.currentSubscription.selected ? RoundedRectangleBorder(
borderRadius: BorderRadius.circular(3.0),
side: BorderSide(color: buttonBackground, width: 2.0),
) : ContinuousRectangleBorder(),
color: bgDarkColor,
child: SizedBox(
width: SizeConfig.blockSizeHorizontal * 30,
child: Stack(
alignment: Alignment.topRight,
children: [
Row(
mainAxisSize: MainAxisSize.max,
children: [
Image.asset(this.widget.currentSubscription.logo, height: 35.0,),
Text(
' ${this.widget.currentSubscription.name}',
style: TextStyle(fontFamily: 'Muli', fontSize: 16.0)
),
],
),
widget.currentSubscription.selected ? Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: buttonBackground,
),
child: Padding(
padding: const EdgeInsets.all(2.0),
child: Icon(
Icons.check,
size: 10.0,
color: Colors.white,
),
),
) : Container()
],
),
),
),
);
This is the ListView where the selected cards' images are rendered:
Container(
height: 50,
width: 350,
child: ListView(
scrollDirection: Axis.horizontal,
children: [
IconButton(
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) {
return PopupSubscription();
}
);
},
icon: Icon(Icons.add_box_rounded, size: 30.0,),
),
StatefulBuilder(
builder: (context, setState) {
return ListView.builder(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
itemCount: subscriptionsList.length,
itemBuilder: (context, index) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 5.0),
child: ClipRRect(
borderRadius: BorderRadius.circular(25.0),
child: Image.asset(
subscriptionsList[index].selected ? subscriptionsList[index].logo
: "assets/elements/streaming-services/netflix.jpeg",
),
),
);
},
);
}
),
],
)
),
Based on your current code, I'm guessing you've added the currentSubscription variable as final in the StatefulWidget above the actual State, like:
class MyClass extends StatefulWidget{
final currentSubscription;
// Rest of the code
}
class _MyClassState extends State<MyClass> {
// Your ListView Code and other stuff.
}
This wont work when you want to change the state onTap, I recommend making the variable you use in setState within the _MyClassState class and use setState in that. Something like:
class _MyClassState extends State<MyClass> {
bool _isSelected = false;
// And in your onTap method, something like:
setState(() {
_isSelected = !_isSelected;
});
}

How to put an iOS style dismissible bar on flutter modal sheet

I am pretty new to flutter and trying to achieve a dismissible bar for my modalSheet. Something like this image:
I can only think of a stack. But that would make the code complex. Please let me know if there is a better way.
Is this what you are looking for modal_bottom_sheet:
showBarModalBottomSheet(
expand: true,
context: context,
backgroundColor: Colors.transparent,
builder: (context, scrollController) =>
ModalInsideModal(
scrollController: scrollController),
)),
To display showModalBottomSheet as IOS modal style:
1- Create new Dart Class IOSModalStyle with this full code:
import 'package:flutter/material.dart';
class IOSModalStyle extends StatelessWidget {
final Widget childBody;
const IOSModalStyle({
Key? key,
required this.childBody,
}) : super(key: key);
#override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.all(16.0),
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Colors.transparent,
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_dividerWidget(),
Container(
decoration: BoxDecoration(
color: Colors.white, // color of card
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
),
height: 200, // body container height
width: double.infinity,
child: childBody,
)
],
),
);
}
Widget _dividerWidget() {
return FractionallySizedBox(
widthFactor: 0.2, // width of top divider bar
child: Container(
margin: const EdgeInsets.symmetric( // margin of top divider bar
vertical: 12.0,
),
child: Container(
height: 5.0,
decoration: BoxDecoration(
color: Colors.white, // color of top divider bar
borderRadius: const BorderRadius.all(Radius.circular(2.5)),
),
),
),
);
}
}
2- You can call above class from anywhere like this:
showModalBottomSheet<void>(
isScrollControlled: true, // to full height
useSafeArea: true, // to show under status bar
backgroundColor: Colors.transparent, // to show BorderRadius of Container
context: context,
builder: (BuildContext context) {
return IOSModalStyle(
childBody: Center(
child: Text('Hello, Im Anas...'),
),
);
},
);
Result:

How to add Hero animations to my ListView in Flutter?

I have a ListView that has cards as shown below. I am trying to animate the card to open in a bigger size upon being pressed.
I have already achieved this for a single card however I can not understand how to do this for a ListView. Upon adding the Hero tag on the ListView elements, I get the following error:
There are multiple heroes that share the same tag within a subtree.
This is the animation.
This is the code for the animation:
The first Page:
Widget build(BuildContext context) {
return new Scaffold(
body: Hero(
tag: 'flutterLogo',
child: GestureDetector(
onTap: () => Navigator.push(context,
MaterialPageRoute(builder: (context) => AnimatedPage())),
child: Card(
elevation: 4.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
child: Container(
height: 180,
width: 300,
color: Colors.blue,
),
),
)),
);
}
The Second Page:
Widget build(BuildContext context) {
return new Scaffold(
body: Stack(
children: <Widget>[
Hero(
tag: 'flutterLogo',
child: Padding(
padding: const EdgeInsets.only(top: 20.0),
child: Card(
elevation: 4.0,
color: Colors.red,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
child: Container(
height: 200,
width: 300,
child: Center(child: Text('Hello')),
),
),
),
),
],
),
);
}
And this is the code for my ListView:
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Hero(
tag: 'flutterLogo',
child: Padding(
padding: const EdgeInsets.only(top: 10.0, bottom: 10),
child: GestureDetector(
onTap: () {
Navigator.push(context,
MaterialPageRoute(builder: (context) => PaymentPage()));
},
child: Card(
elevation: 4.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
child: Container(
height: 180,
width: 150,
),
),
),
),
);
},
childCount: 4,
),
),
And this is what the list looks like:
I am trying to open the same
second page as shown in the Animation, after clicking on the card in the List.
Hero Tags should be different, so in your SliverList change your tag to this:
tag: 'flutterLogo${index}',
and pass the tag to your second page, like this(in case its a StatefulWidget):
class SecondPage extends StatefulWidget {
final String heroTag;
SecondPage({Key key, this.heroTag}) : super(key: key);
#override
_SecondPageState createState() => _SecondPageState();
}
and then instead of using a simple tag in your second page use something like this:
tag: widget.heroTag,

Flutter custom bottom sheet

I have a ListView where I want to implement a nice way to delete list items using a bottom sheet with actions on. Initially I went down the path of simply calling showBottomSheet() in the onLongPress event handler for my list items, which would successfully open a bottom sheet with my action buttons on. However, this would automatically add a back button to the AppBar which is not what I want.
I then went down the route of trying out animations, such as SlideTransition and AnimatedPositioned:
class FoldersListWidget extends StatefulWidget {
#override
_FoldersListWidgetState createState() => _FoldersListWidgetState();
}
class _FoldersListWidgetState extends State<FoldersListWidget>
with SingleTickerProviderStateMixin {
double _bottomPosition = -70;
#override
Widget build(BuildContext context) {
return Stack(
children: [
FutureBuilder<List<FolderModel>>(
future: Provider.of<FoldersProvider>(context).getFolders(),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
}
return ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, i) {
final folder = snapshot.data[i];
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
margin: EdgeInsets.symmetric(vertical: 5, horizontal: 10),
elevation: 1,
child: ListTile(
title: Text(folder.folderName),
leading: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
width: 50,
child: Consumer<FoldersProvider>(
builder:
(BuildContext context, value, Widget child) {
return value.deleteFolderMode
? CircularCheckBox(
value: false,
onChanged: (value) {},
)
: Icon(
Icons.folder,
color: Theme.of(context).accentColor,
);
},
),
),
],
),
subtitle: folder.numberOfLists != 1
? Text('${folder.numberOfLists} items')
: Text('${folder.numberOfLists} item'),
onTap: () {},
onLongPress: () {
Provider.of<FoldersProvider>(context, listen: false)
.toggleDeleteFolderMode(true); // removes fab from screen
setState(() {
_bottomPosition = 0;
});
},
),
);
},
);
},
),
AnimatedPositioned(
bottom: _bottomPosition,
duration: Duration(milliseconds: 100),
child: ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(25),
topRight: Radius.circular(25),
),
child: Container(
height: 70,
width: MediaQuery.of(context).size.width,
color: Theme.of(context).colorScheme.surface,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
Expanded(
child: IconAboveTextButton(
icon: Icon(Icons.cancel),
text: 'Cancel',
textColour: Colors.black,
opacity: 0.65,
onTap: () => setState(() {
_bottomPosition = -70;
}),
),
),
VerticalDivider(
color: Colors.black26,
),
Expanded(
child: IconAboveTextButton(
icon: Icon(Icons.delete),
text: 'Delete',
textColour: Colors.black,
opacity: 0.65,
),
),
],
),
),
),
),
],
);
}
}
This slides the bottom Container on and off the screen but my issue is that it covers the last list item:
Could anyone suggest a better way of doing this or simply a way to adjust the height of the ListView so that when the Container slides up, the ListView also slides up.
Wrap ListView.builder inside a container and set its bottom padding as (70+16)
70 (height of bottom sheet), 16 (some default padding to make it look better if you like).
return Container(
padding: EdgetInset.ony(bottom: (70+16)),
child:ListView.builder(
itemCount: snapshot.data.length,
itemBuilder: (context, i) {
.....
.....