I am having an issue where the keyboard covers and hides the bottom sheet.
I tried a number of solutions but nothing seems to work. I am not too sure what is causing the problem. My guess is that it has something to do with either nested Scaffold and/or using bottom sheet in TabBarView.
Any help would be greatly appreciated.
I tried to include as much detail as possible. If you need anything else, please let me know.
Code below:
DetailsPage.dart
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
key: _scaffoldKey,
appBar: AppBar(
centerTitle: true,
title: UtilWidget.getLogo(),
bottom: TabBar(
labelColor: Colors.deepPurpleAccent,
unselectedLabelColor: Colors.white,
indicatorSize: TabBarIndicatorSize.label,
indicator: BoxDecoration(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(10)),
color: Colors.white),
tabs: [
UtilWidget.addTab('Details'),
UtilWidget.addTab('Procedures'),
],
),
),
body: TabBarView(
children: <Widget>[
EditServiceFragment(contractorServiceId: widget.contractorServiceId),
ServiceProceduresFragment(contractorServiceId: widget.contractorServiceId)
],
),
)
);
}
ServiceProceduresFragment.dart
#override
Widget build(BuildContext context) {
return Scaffold(
primary: false,
resizeToAvoidBottomInset: true,
key: _scaffoldKey,
body: _buildPage(),
floatingActionButton: new FloatingActionButton.extended(
onPressed: () {
_addProcedureBottomSheet(context);
},
label: Text('Add Procedure'),
icon: new Icon(Icons.add),
),
);
}
Widget _buildPage() {
return FutureBuilder(
future: _loadedContractorService,
builder: (BuildContext context, AsyncSnapshot<ContractorService> serviceRes) {
if(serviceRes.hasError) {
print('Error while loading Asset - EditService');
print(serviceRes.error);
return UtilWidget.pageLoadError(
"SERVICE PROCEDURES",
"An Error has occurred",
"Got an error getting Contractor Service from the API."
);
}
if (serviceRes.connectionState == ConnectionState.waiting)
return UtilWidget.progressIndicator();
ContractorService loadedService = serviceRes.data;
List<Procedure> procedures = loadedService.procedures;
if(procedures.length == 0)
return UtilWidget.emptyListView('SERVICE PROCEDURES', 'No procedures were found. Add one.');
return SingleChildScrollView(
child: ListView.builder(
shrinkWrap: true,
itemCount: procedures.length + 1,
itemBuilder: (context, index) {
if(index == 0){
return UtilWidget.pageHeader('SERVICE PROCEDURES');
}
int listIndex = index - 1;
Procedure procedure = procedures[listIndex];
return Card(
elevation: 2,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20.0)),
margin: EdgeInsets.only(top: 10, bottom: 10, left: 20, right: 20),
child: Theme(
data: ThemeData().copyWith(
dividerColor: Colors.transparent,
accentColor: Colors.deepPurpleAccent
),
child: ExpansionTile(
title: ListTile(
title: Text(
procedure.name,
style: TextStyle(fontWeight: FontWeight.bold)
),
subtitle: Text(
'Requirements: ' +
procedure.requirements.length.toString()
),
),
childrenPadding: EdgeInsets.only(left: 20, right: 20),
children: [
UtilWidget.addRowWithText('Notes', FontWeight.bold),
UtilWidget.addRowWithText(procedure.notes, FontWeight.normal),
_deleteProcedureBtn(procedure.id),
Padding(
padding: const EdgeInsets.only(top: 10),
child: UtilWidget.addRowWithText('Requirements', FontWeight.bold),
),
for(var req in procedure.requirements)
UtilWidget.addRowWithText(req.name, FontWeight.normal)
,
_addRequirementBtn(procedure.id)
]
)
)
);
}
)
);
}
);
}
void _addProcedureBottomSheet(BuildContext context) {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (BuildContext bc) {
return SafeArea(
child: SingleChildScrollView(
padding: EdgeInsets.only(
top: 10, left:15, right:15,
bottom: MediaQuery.of(context).viewInsets.bottom
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Center(
child: Text(
'Add Procedure to Service',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18
),
),
),
_addProcedureForm()
],
),
)
);
}
);
}
Form _addProcedureForm() {
return Form(
key: _formKey,
child: Column(
children: <Widget>[
TextFormField(
validator: (value) {
if(value.isEmpty) {
return 'Service name is required.';
}
return null;
},
controller: _name,
decoration: InputDecoration(
labelText: 'Name',
hintText: "Specific/applicable procedure",
icon: Icon(Icons.edit, color: Colors.deepPurple)
)
),
TextFormField(
validator: (value) {
if(value.isEmpty) {
return 'Service description is required.';
}
return null;
},
maxLines: null,
keyboardType: TextInputType.multiline,
controller: _notes,
decoration: InputDecoration(
labelText: 'Description',
hintText: "Applicable procedure description.",
icon: Icon(Icons.notes, color: Colors.deepPurple)
)
),
Container(
margin: const EdgeInsets.only(top: 20.0, bottom: 10),
child: SizedBox(
width: double.maxFinite,
child: RaisedButton(
color: Colors.deepPurple,
textColor: Colors.white,
padding: EdgeInsets.all(5),
child: Text('Save Procedure',
style: TextStyle(fontSize: 15)
),
onPressed: () {
if(_formKey.currentState.validate()) {
_submitProcedureForm();
}
},
),
),
)
],
),
);
}
Widget _addRequirementBtn(int procedureId) {
return Padding(
padding: const EdgeInsets.only(top: 10.0, bottom: 10),
child: RaisedButton(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20.0)),
color: Colors.deepPurple,
textColor: Colors.white,
child: Text('Add Requirement',
style: TextStyle(fontSize: 14)
),
onPressed: () {
_addProcedureRequirementBottomSheet(procedureId, context);
}
)
);
}
Outputs
You can check the value of MediaQuery.of(context).viewInsets.bottom to know whether the keyboard is active and put some space to the bottom of the modal.
Also, you can use AnimatedPadding to make it look smooth.
void _addProcedureBottomSheet(BuildContext context) {
showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (BuildContext context) {
var keyboardHeight = MediaQuery.of(context).viewInsets.bottom ?? 0.0;
return AnimatedPadding(
padding: EdgeInsets.only(bottom: keyboardHeight),
duration: Duration(milliseconds: 300),
child: SafeArea(
bottom: keyboardHeight <= 0.0,
child: SingleChildScrollView(
padding: EdgeInsets.only(
top: 10,
left: 15,
right: 15,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Center(
child: Text(
'Add Procedure to Service',
style:
TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
),
),
_addProcedureForm()
],
),
),
),
);
},
);
}
Thanks to Stewie, I realized that MediaQuery.of(context).viewInsets.bottom wasn't registering the appearance of the keyboard.
I was able to solve the issue by passing in the BuildContext from the page with DefaultTabController to the TabView page. Using this BuildContext resolved the issue.
Code below:
ContractorServiceDetailsPage.dart
#override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
key: _scaffoldKey,
appBar: AppBar(
centerTitle: true,
title: UtilWidget.getLogo(),
bottom: TabBar(
labelColor: Colors.deepPurpleAccent,
unselectedLabelColor: Colors.white,
indicatorSize: TabBarIndicatorSize.label,
indicator: BoxDecoration(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(10)),
color: Colors.white),
tabs: [
UtilWidget.addTab('Details'),
UtilWidget.addTab('Procedures'),
],
),
),
body: TabBarView(
children: <Widget>[
EditServiceFragment(contractorServiceId: widget.contractorServiceId),
ServiceProceduresFragment(
baseContext: context,
contractorServiceId: widget.contractorServiceId
)
],
),
)
);
}
ServiceProceduresFragment.dart
class ServiceProceduresFragment extends StatefulWidget {
final BuildContext baseContext;
final int contractorServiceId;
const ServiceProceduresFragment({
Key key,
this.contractorServiceId,
this.baseContext}) : super(key: key);
#override
State<StatefulWidget> createState() {
return _ServiceProceduresState();
}
}
class _ServiceProceduresState extends State<ServiceProceduresFragment> {
...
#override
Widget build(BuildContext context) {
return Scaffold(
primary: false,
resizeToAvoidBottomPadding: false,
key: _scaffoldKey,
body: _buildPage(),
floatingActionButton: new FloatingActionButton.extended(
onPressed: () {
_addProcedureBottomSheet(widget.baseContext);
},
label: Text('Add Procedure'),
icon: new Icon(Icons.add),
),
);
}
}
Did you try to wrap your SafeArea widget with Padding and adding
padding like :
=> ShowBottomSheet.instance.showBottomSheet(
isScrollControlled: true,
context: context,
widget: Padding(
padding: MediaQuery.of(context).viewInsets,
child: Container(
height: MediaQuery.of(context).size.height * 0.60,
child: DefaultTabController(
length: 2,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: MySize.size12),
child: Column(children: <Widget>[
const TabsOfTabbar(),
....... );
Related
I'm new to Flutter, but I'm trying to make an app with a ListView. The ListView is a list of exercises, within each exercise the number of sets can be added. The problem comes when i press the button add exercise. The above exercise with sets is just copied. I would like a new exercise tab with 0 sets. Below the code can be found.
Here is a picture of the list.
final decoratedField = InputDecoration(
filled: false,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20.0),
),
hintText: "null",
);
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Column(
children: [
titleSection,
// ignore: unnecessary_new
new TextField(
controller: eCtrl,
onSubmitted: (text) {
litems.add(text);
eCtrl.clear();
setState(() {});
},
),
Expanded(
// ignore: unnecessary_new
child: new ListView.builder(
itemCount: litems.length,
itemBuilder: (BuildContext ctxt, int Index) {
return Card(
child: Padding(
padding: EdgeInsets.all(10),
child: ExpansionTile(
initiallyExpanded: true,
title: Text(
litems[Index],
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
subtitle: Row(
children: [
Expanded(child: Text(" ")),
//Expanded(child: Text("data")),
//Expanded(child: Text("data")),
//Expanded(child: Text("data")),
],
),
// ignore: sort_child_properties_last
children: <Widget>[
ListView.builder(
shrinkWrap: true,
itemCount: sets.length,
itemBuilder:
(BuildContext context, int Index1) {
return Dismissible(
key: UniqueKey(),
// only allows the user swipe from right to left
direction:
DismissDirection.endToStart,
// Remove this product from the list
// In production enviroment, you may want to send some request to delete it on server side
onDismissed: (_) {
setState(() {
sets.removeAt(Index1);
});
},
// ignore: sort_child_properties_last
child: Card(
elevation: 0,
child: Padding(
padding: EdgeInsets.all(1),
child: ListTile(
title: Text(
" ",
style: const TextStyle(
fontSize: 10,
fontWeight:
FontWeight.bold,
),
),
subtitle: Row(
children: [
Expanded(
child: Text(" "),
),
Expanded(
child: TextField(
decoration:
decoratedField,
),
),
Expanded(
child: TextField(
decoration:
decoratedField,
),
),
Expanded(
child: TextField(
decoration:
decoratedField,
),
),
],
),
))),
background: Container(
color: Colors.red,
margin:
const EdgeInsets.symmetric(
horizontal: 15,
),
alignment: Alignment.centerRight,
child: const Text(
"Delete",
style: TextStyle(
color: Colors.white,
),
)));
}),
Padding(
padding: EdgeInsets.all(10),
child: ElevatedButton(
onPressed: () {
sets.add('sets-test');
setState(() {});
},
child: const Text('+ Add Set')),
),
const SizedBox(height: 5),
],
leading: IconButton(
icon: const Icon(
Icons.close,
color: Colors.red,
),
onPressed: () {
litems.removeAt(Index);
setState(() {});
},
),
)));
})),
ElevatedButton(
onPressed: () {
litems.add("new");
setState(() {});
},
child: const Text('Add Exercises')),
ElevatedButton(
onPressed: () {
createUser(user1, "5");
exercise.setExerciseTotals();
//saveExercise(exercise);
final workout = Workout([exercise, exercise1], "Det gik fint",
"10", 60, "type", "name", true, 0, 0, 0);
//workout.setWorkoutTotals();
saveWorkout(workout, userID);
},
child: const Text('pop')),
bottomSection,
],
),
));
}
You are not copy the item, you logic is that add new Item with null value, change decoratedField to this:
final decoratedField = InputDecoration(
filled: false,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20.0),
),
hintText: "0",
);
I have a statelsess widget built like this where a futurebuilder fetches data and then returns a StatefullTextFormFieldWidget:
Statless Widget{
build:
Futurebuilder(snapshot, context){
future: GetDocumentFromFirebase(id);
if(snapshot.hasData){
return StatefullTextFormFieldWidget(snapshot);
}
}
}
The StatefullTextFormfieldWidget contains a appbar and scaffold with with 2 TextFormFields and workes as it should (when it was offline).
The bug occurs when any of my forms gets onFocusScope in the StatefullTextFormFieldWidget. Then the future starts to refetch data from firebase. It does NOT trigger the rebuild function so my app actually works fine since the state remains, but the main problem is that the app unnecessarily starts fetching data from firestore everytime the user clicks on a TextFormField. I only want to fetch data once when the user enter the screen and then stick with that snapshot as long as the user remains on the screen.
I really cant understand how the futurebuilder can continue fetch data without being rebuilt.
FuturebuilderWidget:
import 'package:flutter/material.dart';
import 'package:MyApp/manage_orders/handle/widgets/form_has_handling.dart';
import 'package:MyApp/services/firestore.dart';
import 'package:MyApp/services/models.dart';
class HandleOrderScreen extends StatefulWidget {
static const routeName = '/handle-order-screen';
#override
State<HandleOrderScreen> createState() => _HandleOrderScreenState();
}
class _HandleOrderScreenState extends State<HandleOrderScreen> {
#override
Widget build(BuildContext context) {
final String id = ModalRoute.of(context)!.settings.arguments as String;
return FutureBuilder<Map>(
future: FirestoreService().getOrderPriceUserBoat(id),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Scaffold(
appBar: AppBar(
title: Text(''),
),
body: Center(
child: Center(child: CircularProgressIndicator()),
),
);
} else if (snapshot.hasError) {
return Scaffold(
appBar: AppBar(
title: Text('Error'),
),
body: Center(
child: Text(snapshot.error.toString()),
),
);
} else if (snapshot.hasData) {
var snapshotData = snapshot.data;
Order order = snapshotData!['order'];
return TextFormFieldWidget(order: order);
} else {
return Scaffold(
appBar: AppBar(
title: Text('Error'),
),
body: Center(
child: Text('Something went wrong'),
),
);
}
});
}
}
TextFormFieldWidget:
import 'package:flutter/material.dart';
import 'package:MyApp/services/models.dart';
class TextFormFieldWidgetextends StatefulWidget {
const TextFormFieldWidget({
Key? key,
required this.order,
}) : super(key: key);
final Order order;
#override
State<FormHasHandlingWidget> createState() => _TextFormFieldWidgetState();
}
class _TextFormFieldWidgetState extends State<TextFormFieldWidget> {
final commentController = TextEditingController();
final priceController = TextEditingController();
static final formkey = GlobalKey<FormState>();
#override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: Text('Handle'),
),
body: SingleChildScrollView(
child: Column(
children: [
Container(
padding: EdgeInsets.all(5),
height: size.height - 120,
child: Form(
key: formkey,
child: Column(
children: [
Container(
padding: EdgeInsets.all(2),
width: double.infinity,
height: 120,
child: Card(
color: Color.fromARGB(255, 241, 235, 235),
child: Container(
child: RichText(
text: TextSpan(
style:
TextStyle(color: Colors.black, fontSize: 20),
children: <TextSpan>[
TextSpan(
text: 'Test Name\n',
),
TextSpan(
text: 'Order.nr:',
style: TextStyle(fontWeight: FontWeight.bold),
),
TextSpan(
text: '${widget.order.orderNumber} \n',
),
TextSpan(
text: 'Car: ',
style: TextStyle(fontWeight: FontWeight.bold),
),
TextSpan(
text: widget.order.carModel + '\n',
),
],
),
),
),
),
),
Container(
padding: EdgeInsets.all(2),
child: Align(
alignment: Alignment.topLeft,
child: Text(
" Type:",
style: TextStyle(color: Colors.grey),
),
),
),
SizedBox(
height: 10,
),
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.black, width: 2),
),
child: Container(
padding: EdgeInsets.all(5),
child: TextFormField(
controller: priceController,
decoration: InputDecoration(labelText: 'Price: '),
validator: (value) {
if (value != null) {
if (value.length < 1) {
return 'Wright the price';
} else {
return null;
}
}
},
keyboardType: TextInputType.number,
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 20),
),
),
),
SizedBox(
height: 10,
),
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.black, width: 2),
),
child: Container(
padding: EdgeInsets.all(5),
child: TextFormField(
controller: commentController,
decoration:
InputDecoration(labelText: 'Description:'),
keyboardType: TextInputType.multiline,
style: TextStyle(
fontWeight: FontWeight.bold, fontSize: 20),
minLines: 2,
maxLines: 5,
validator: (value) {
if (value != null) {
if (value.length < 10) {
return 'Give a short description';
} else {
return null;
}
}
},
),
),
),
Container(
padding: EdgeInsets.all(5),
child: ElevatedButton(
child: Text('Send in'),
onPressed: () {},
style: ElevatedButton.styleFrom(
fixedSize: Size(size.width, 50),
primary: Color.fromARGB(255, 223, 208, 0)),
),
),
Spacer(),
Container(
padding: EdgeInsets.all(5),
child: ElevatedButton(
child: Text('Finish order'),
onPressed: () {},
style: ElevatedButton.styleFrom(
fixedSize: Size(size.width, 50),
primary: Color.fromARGB(255, 255, 17, 0)),
),
),
],
),
),
),
SizedBox(
height: 200,
),
],
),
),
);
}
}
My guess is that your GetDocumentFromFirebase function does a new get() call to Firestore each time it runs, which happens every time the widget is rendered.
If you want to prevent refetching the document every time, put the code in a stateful widget, and keep the Future<DocumentSnapshot> in a state variable that you only initialize once.
Also see Randal's excellent explanation on Fixing a common FutureBuilder and StreamBuilder problem
I am working on dropdownmenu items where in the drop-down menu item there are several checkboxes but any of the checkboxes on hover don't give on tap cursor permission.
This is a very strange thing I found out as I have already used the checkbox before but this type of error I didn't receive.
I think maybe the problem is in dropdownmenu.
I have also included the video for better understanding of my problem.
my code :-
Container(
width: 160,
//margin: const EdgeInsets.only(top: 10.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(5), color: Colors.white),
child: ListTileTheme(
contentPadding: EdgeInsets.all(0),
dense: true,
horizontalTitleGap: 0.0,
minLeadingWidth: 0,
child: ExpansionTile(
iconColor: primaryBackgroundLightGrey,
title: Text(
listOFSelectedItem.isEmpty
? "Project type"
: listOFSelectedItem[0],
style: t5O40),
children: <Widget>[
Container(
height: 10,
color: primaryBackgroundLightGrey,
),
ListView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: widget.listOFStrings.length,
itemBuilder: (BuildContext context, int index) {
return Column(
children: [
Container(
height: 10,
),
Container(
margin: const EdgeInsets.only(bottom: 8.0),
child: _ViewItem(
item: widget.listOFStrings[index],
selected: (val) {
selectedText = val;
if (listOFSelectedItem.contains(val)) {
listOFSelectedItem.remove(val);
} else {
listOFSelectedItem.add(val);
}
widget.selectedList(listOFSelectedItem);
setState(() {});
},
itemSelected: listOFSelectedItem
.contains(widget.listOFStrings[index])),
),
],
);
},
),
],
),
),
),
class _ViewItem extends StatelessWidget {
String item;
bool itemSelected;
final Function(String) selected;
_ViewItem(
{required this.item, required this.itemSelected, required this.selected});
#override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
return Padding(
padding: EdgeInsets.only(
left: size.width * .015,
),
child: Row(
children: [
SizedBox(
height: 2,
width: 2,
child: Checkbox(
value: itemSelected,
onChanged: (val) {
selected(item);
},
hoverColor: Colors.transparent,
checkColor: Colors.white,
activeColor: Colors.grey),
),
SizedBox(
width: size.width * .010,
),
Text(item, style: t3O60),
],
),
);
}
}
You can adapt the example to your own code
dropdownBuilder: _customDropDownExample,
popupItemBuilder: _customPopupItemBuilderExample,
Widget _customDropDownExample(
BuildContext context, UserModel? item, String itemDesignation) {
if (item == null) {
return Container();
}
return Container(
child: (item.avatar == null)
? ListTile(
contentPadding: EdgeInsets.all(0),
leading: CircleAvatar(),
title: Text("No item selected"),
)
: ListTile(
contentPadding: EdgeInsets.all(0),
leading: CircleAvatar(
// this does not work - throws 404 error
// backgroundImage: NetworkImage(item.avatar ?? ''),
),
title: Text(item.name),
subtitle: Text(
item.createdAt.toString(),
),
),
);
After that
Widget _customPopupItemBuilderExample(
BuildContext context, UserModel item, bool isSelected) {
return Container(
margin: EdgeInsets.symmetric(horizontal: 8),
decoration: !isSelected
? null
: BoxDecoration(
border: Border.all(color: Theme.of(context).primaryColor),
borderRadius: BorderRadius.circular(5),
color: Colors.white,
),
child: ListTile(
selected: isSelected,
title: Text(item.name),
subtitle: Text(item.createdAt.toString()),
leading: CircleAvatar(
// this does not work - throws 404 error
// backgroundImage: NetworkImage(item.avatar ?? ''),
),
),
);
I am using this package https://pub.dev/packages/dropdown_button2
Multiselect Dropdown with Checkboxes
final List<String> items = [
'Item1',
'Item2',
'Item3',
'Item4',
];
List<String> selectedItems = [];
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: DropdownButtonHideUnderline(
child: DropdownButton2(
isExpanded: true,
hint: Align(
alignment: AlignmentDirectional.center,
child: Text(
'Select Items',
style: TextStyle(
fontSize: 14,
color: Theme.of(context).hintColor,
),
),
),
items: items.map((item) {
return DropdownMenuItem<String>(
value: item,
//disable default onTap to avoid closing menu when selecting an item
enabled: false,
child: StatefulBuilder(
builder: (context, menuSetState) {
final _isSelected = selectedItems.contains(item);
return InkWell(
onTap: () {
_isSelected
? selectedItems.remove(item)
: selectedItems.add(item);
//This rebuilds the StatefulWidget to update the button's text
setState(() {});
//This rebuilds the dropdownMenu Widget to update the check mark
menuSetState(() {});
},
child: Container(
height: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
_isSelected
? const Icon(Icons.check_box_outlined)
: const Icon(Icons.check_box_outline_blank),
const SizedBox(width: 16),
Text(
item,
style: const TextStyle(
fontSize: 14,
),
),
],
),
),
);
},
),
);
}).toList(),
//Use last selected item as the current value so if we've limited menu height, it scroll to last item.
value: selectedItems.isEmpty ? null : selectedItems.last,
onChanged: (value) {},
buttonHeight: 40,
buttonWidth: 140,
itemHeight: 40,
itemPadding: EdgeInsets.zero,
selectedItemBuilder: (context) {
return items.map(
(item) {
return Container(
alignment: AlignmentDirectional.center,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Text(
selectedItems.join(', '),
style: const TextStyle(
fontSize: 14,
overflow: TextOverflow.ellipsis,
),
maxLines: 1,
),
);
},
).toList();
},
),
),
),
);
}
I have a working filter button in search page of my app
I need to add it as floating button in other pages such as category, view all products etc
Here is the working filter button code for searchscreen.
class SearchProductWidget extends StatelessWidget {
final bool isViewScrollable;
final List<Product> products;
SearchProductWidget({this.isViewScrollable, this.products});
#override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(Dimensions.PADDING_SIZE_SMALL),
child: Column(
children: [
Row(
children: [
Expanded(
child: Text(
'Search result for \"${Provider.of<SearchProvider>(context).searchText}\" (${products.length} items)',
style: titilliumRegular.copyWith(
fontSize: Dimensions.FONT_SIZE_DEFAULT),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
InkWell(
onTap: () => showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (c) => SearchFilterBottomSheet()),
child: Container(
padding: EdgeInsets.symmetric(
vertical: Dimensions.PADDING_SIZE_EXTRA_SMALL,
horizontal: Dimensions.PADDING_SIZE_SMALL),
decoration: BoxDecoration(
color: ColorResources.getLowGreen(context),
borderRadius: BorderRadius.circular(5),
border: Border.all(
width: 1, color: Theme.of(context).hintColor),
),
child: Row(children: [
///Image.asset(Images.filter_image, width: 10, height: 10, color: ColorResources.getPrimary(context)),
SizedBox(width: Dimensions.PADDING_SIZE_EXTRA_SMALL),
Text('Filter'),
]),
),
),
],
),
SizedBox(height: Dimensions.PADDING_SIZE_SMALL),
Expanded(
child: StaggeredGridView.countBuilder(
physics: BouncingScrollPhysics(),
padding: EdgeInsets.all(0),
crossAxisCount: 2,
itemCount: products.length,
//shrinkWrap: true,
staggeredTileBuilder: (int index) => StaggeredTile.fit(1),
itemBuilder: (BuildContext context, int index) {
return ProductWidget(productModel: products[index]);
},
),
),
],
),
);
}
}
I'm trying to create a floating action button to work as a filter in different screens
Here is one of the screen which I need the filter button working-
class AllProductScreen extends StatelessWidget {
final ScrollController _scrollController = ScrollController();
final ProductType productType;
AllProductScreen({#required this.productType});
// Future<void> _loadData(BuildContext context, bool reload) async {
// String _languageCode = Provider.of<LocalizationProvider>(context, listen: false).locale.countryCode;
// await Provider.of<BrandProvider>(context, listen: false).getBrandList(reload, context);
// await Provider.of<ProductProvider>(context, listen: false).getLatestProductList('1', context, _languageCode, reload: reload);
//
//
//
// }
#override
Widget build(BuildContext context) {
// _loadData(context, false);
return Scaffold(
backgroundColor: ColorResources.getHomeBg(context),
resizeToAvoidBottomInset: false,
appBar: AppBar(
backgroundColor: Provider.of<ThemeProvider>(context).darkTheme
? Colors.black
: Theme.of(context).primaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
bottomRight: Radius.circular(5),
bottomLeft: Radius.circular(5))),
leading: IconButton(
icon:
Icon(Icons.arrow_back_ios, size: 20, color: ColorResources.WHITE),
onPressed: () => Navigator.of(context).pop(),
),
title: Text(
productType == ProductType.FEATURED_PRODUCT
? 'Featured Product'
: 'Latest Product',
style: titilliumRegular.copyWith(
fontSize: 20, color: ColorResources.WHITE)),
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () => showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (c) => SearchFilterBottomSheet()),
icon: const Icon(Icons.filter_list),
label: const Text('Filter'),
),
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
body: SafeArea(
child: RefreshIndicator(
backgroundColor: Theme.of(context).primaryColor,
onRefresh: () async {
// await _loadData(context, true);
return true;
},
child: CustomScrollView(
controller: _scrollController,
slivers: [
SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.all(Dimensions.PADDING_SIZE_SMALL),
child: ProductView(
isHomePage: false,
productType: productType,
scrollController: _scrollController),
),
),
],
),
),
),
);
}
}
The exception I'm getting is
════════════════════════════════════════════════════════════════════════════════
════════ Exception caught by gesture ═══════════════════════════════════════════
The getter 'iterator' was called on null.
Receiver: null
Tried calling: iterator
When the class members of child class PartnerInviter are changed inside the child class, somehow it changes the state of the parent class as well which is PartnerConnect. isInviteOption: true, isInviteSent: false => Parent widget resets the flag to false, false. I thing build of PartnerConnect is getting called. How do I fix this?
Parent widget is:
class PartnerConnect extends StatefulWidget{
bool isInviter = true;
#override
State<StatefulWidget> createState() {
return PartnerConnectState();
}
}
class PartnerConnectState extends State<PartnerConnect>{
final _scaffoldKey = GlobalKey<ScaffoldState>();
double card_elevation = 1;
TextEditingController phoneNumber = TextEditingController();
#override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
key: _scaffoldKey,
body: widget.isInviter ? PartnerInviter(
isInviteOption: false,
isInviteSent: false,
scaffoldKey: _scaffoldKey,
) : PartnerInvitee(
isInvitationAccepted: true,
inviterNumber: '9********0',
scaffoldKey: _scaffoldKey),
appBar: AppBar(
title: Text('Partner Connect'),
backgroundColor: Colors.white,
elevation: 0.0,
),
backgroundColor: Colors.white,
),
);
}
}
Child widget is:
class PartnerInviter extends StatefulWidget{
bool isInviteOption = false;
bool isInviteSent = false;
GlobalKey<ScaffoldState> scaffoldKey;
PartnerInviter({this.isInviteOption, this.isInviteSent, this.scaffoldKey});
#override
State<StatefulWidget> createState() {
return _PartnerInviterState();
}
}
class _PartnerInviterState extends State<PartnerInviter> {
double card_elevation = 1;
TextEditingController phoneNumber = TextEditingController();
#override
Widget build(BuildContext context) {
return inviterConnectBody();
}
Widget inviterConnectBody(){
return Container(
margin: EdgeInsets.all(MediaQuery.of(context).size.width*0.1),
child: ListView(
children: <Widget>[
Visibility(
child: numberInputWidget(),
visible: widget.isInviteOption,
replacement: Container(
child: Visibility(
child: RequestReceivedWidget(phoneNum: phoneNumber.value.text),
visible: widget.isInviteSent,
replacement: Container(
child: RequestRevokeWidget(phoneNum: phoneNumber.value.text),
),
),
),
)
],
),
);
}
Widget RequestRevokeWidget({String phoneNum}){
return Card(
elevation: card_elevation,
color: Colors.deepPurple[50],
child: Container(
height: MediaQuery.of(context).size.height * 0.3,
child: ListView(
children: <Widget>[
ListTile(
title: Text('Partner connected'),
subtitle: Text('Registered number: ${phoneNum}'),
),
Center(
child: Container(
margin: EdgeInsets.only(left: MediaQuery.of(context).size.width * 0.04, right: MediaQuery.of(context).size.width * 0.04),
child: RaisedButton(
color: Colors.deepPurple,
child: Padding(
padding: EdgeInsets.only(bottom: 5),
child: Text('Revoke access', style: TextStyle(color: Colors.white),),),
onPressed: (){
setState(() {
widget.isInviteOption = true;
SnackBar snackBar = SnackBar(content: Text('Partness access successfully revoked'),);
widget.scaffoldKey.currentState.showSnackBar(snackBar);
});
},
),
)),
],
),
),);
}
Widget RequestReceivedWidget({String phoneNum}){
return Card(
elevation: card_elevation,
color: Colors.deepPurple[50],
child: Container(
height: MediaQuery.of(context).size.height * 0.3,
child: ListTile(
title: Text('Waiting for partner to accept invite'),
subtitle: Text('Registered number: ${phoneNum}'),
),
),)
;
}
Widget numberInputWidget(){
final buttonMargin = MediaQuery.of(context).size.width * 0.038;
return Card(
elevation: card_elevation,
color: Colors.deepPurple[50],
child: Container(
height: MediaQuery.of(context).size.height * 0.3,
child: ListView(
children: <Widget>[
ListTile(
title: Text('Partner'),
subtitle: Text('Enter the phone number'),
),
Center(
child: Container(
margin: EdgeInsets.only(left: MediaQuery.of(context).size.width * 0.04, right: MediaQuery.of(context).size.width * 0.04),
child: TextField(
controller: phoneNumber,
keyboardType: TextInputType.number,
enabled: true,
maxLength: 10,
maxLengthEnforced: true,
decoration: InputDecoration(
prefixText: '+91-',
contentPadding: EdgeInsets.only(left: 5.0, bottom: 5.0),
),
),
)),
Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom)),
Container(
margin: EdgeInsets.only(left: buttonMargin, right: buttonMargin),
child: RaisedButton(
color: Colors.deepPurple,
elevation: 0,
padding: EdgeInsets.only(bottom: 5.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10.0)),
child: Text(
"Confirm & Proceed",
style: TextStyle(
color: Colors.white,
fontFamily: "Nunito",
fontWeight: FontWeight.bold),
),
onPressed: () async {
RegExp regExp = new RegExp(r'(^[0-9]*$)');
if (phoneNumber.text.isEmpty) {
widget.scaffoldKey.currentState.showSnackBar(SnackBar(
content: Text("Please enter the mobile number")));
} else if (!regExp.hasMatch(phoneNumber.text) || phoneNumber.text.length < 10) {
widget.scaffoldKey.currentState.showSnackBar(SnackBar(
content: Text("Please enter valid mobile number")));
} else {
setState(() {
widget.isInviteOption = false;
widget.isInviteSent = true;
});
}
},
),
),
],
),
),)
;
}
}