I have created a page in which there is search bar and then the listview builder which is building items of List coming from provider class. When I tap on search bar, the keyboard appears ,suddenly disappears and page refreshes. when page is refreshed, first it shows circularprogressIndicator and then regular ListView.builder.
Kindly Help.
Thank you in advance!
import 'package:carstraders/models/cars.dart';
import 'package:carstraders/providers/cars_provider.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class SearchPage extends StatefulWidget {
static const routeName = 'search-page';
const SearchPage({Key? key}) : super(key: key);
#override
_SearchPageState createState() => _SearchPageState();
}
class _SearchPageState extends State<SearchPage> {
var search = TextEditingController();
var list = [];
List<Cars> allItems = [];
String query = '';
bool isFirstTime = true;
#override
void initState() {
allItems = Provider.of<CarsProvider>(context, listen: false).list;
list = allItems;
// TODO: implement initState
super.initState();
}
this is the function which searches the items and updates the list
void searchItem(String val) {
final searched = allItems.where((element) {
final transmission = element.transmission.toLowerCase();
final model = element.model.toLowerCase();
final make = element.make.toLowerCase();
final price = element.price.toString();
final engine = element.engine.toString();
final searchedVal = val.toLowerCase();
if (model.contains(searchedVal) ||
make.contains(searchedVal) ||
price.contains(searchedVal) ||
engine.contains(searchedVal) ||
transmission.contains(searchedVal)) {
return true;
}
return false;
}).toList();
setState(() {
list = searched;
query = val;
});
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Search'),
),
body: FutureBuilder(
future: Provider.of<CarsProvider>(context,listen: false).fetchAndSet(),
builder: (ctx, snap) => snap.connectionState == ConnectionState.waiting
? const Center(
child: CircularProgressIndicator(),
)
: Column(children: [
Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
onChanged: searchItem,
// style: TextStyle(color: Colors.white),
controller: search,
key: const Key('search'),
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
label: const Text('Search'),
suffixIcon: query.isEmpty
? null
: GestureDetector(
child: const Icon(Icons.close),
onTap: () {
setState(() {
query = '';
searchItem('');
search.clear();
FocusScope.of(context).unfocus();
});
},
),
prefixIcon: const Icon(Icons.search_sharp),
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide(
color: Colors.grey,
width: 0.5,
),
),
),
),
),
),
],
),
Expanded(
child: ListView.builder(
itemBuilder: (ctx, i) => buildItem(list[i], context),
itemCount: list.length,
),
)
]),
),
);
}
}
this widget builds the item of listview.builder
Widget buildItem(Cars item, BuildContext context) {
return GestureDetector(
onTap: () =>
Navigator.of(context).pushNamed('search-item-detail', arguments: item),
child: Card(
elevation: 5,
child: Row(
// mainAxisAlignment: MainAxisAlignment.start,
children: [
SizedBox(
width: MediaQuery.of(context).size.width * 0.43,
child: Image.network(item.img),
),
SizedBox(width: MediaQuery.of(context).size.width * 0.02),
Column(
children: [
Text(
item.year.toString() + ' ' + item.make + ' ' + item.model,
style: const TextStyle(
color: Colors.blueAccent,
),
),
SizedBox(
width: MediaQuery.of(context).size.width * 0.5,
child: const Divider(color: Colors.black)),
Row(
children: [
Text('${item.mileage} mi'),
Container(
margin: EdgeInsets.symmetric(
horizontal: MediaQuery.of(context).size.width * 0.03),
decoration: const BoxDecoration(
border: Border(left: BorderSide(color: Colors.grey))),
child: const Text('')),
Text('£${item.price}')
],
),
SizedBox(
width: MediaQuery.of(context).size.width * 0.5,
child: const Divider(color: Colors.black)),
Row(
children: [
Text(' ' + item.plateNo),
Container(
margin: EdgeInsets.symmetric(
horizontal: MediaQuery.of(context).size.width * 0.03),
child: const Text(''),
decoration: const BoxDecoration(
border: Border(left: BorderSide(color: Colors.grey))),
),
Text(item.transmission)
],
)
],
)
],
),
),
);
}
future builder trigger on every build. so when you click on searchBox the keyboard changes screen size and Expanded rebuild futureBuilder.
Solution Easy way .
Replace below part of future Builder
Column
row
Expanded
listView.Builder
with
ListView.builder(
itemBuilder: (ctx, i) {
if(i==0){
return row
}
return buildItem(list[i -1], context)
},
itemCount: list.length + 1,
),
I think your code can write better. if you use bloc pattern or Riverpod pattern(similar to provider and has same author)
so please look at this links
https://bloclibrary.dev/#/
https://riverpod.dev/
What is the difference between functions and classes to create reusable widgets?
Related
My code includes FutureBuilder(), which get data from Firestore, and its child widgets include GridView.builder and TextField widgets etc.
When I click on a TexField(focus), the codes in FutureBuilder are rebuild.
The following is the test code for this.
Can you tell me the cause and solution of this problem?
class TestRoom extends StatefulWidget {
TestRoom({Key? key}) : super(key: key);
#override
State<TestRoom> createState() => _TestRoomState();
}
class _TestRoomState extends State<TestRoom> {
List<RoomModel> _roomModels = [];
TextEditingController _textEditingController = TextEditingController();
bool _isTablet = false;
#override
void dispose() {
_textEditingController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
Size size = MediaQuery.of(context).size;
if (size.width >= 800) {
_isTablet = true;
} else {
_isTablet = false;
}
return Scaffold(
appBar: AppBar(),
body: Padding(
padding: EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Test",
style: TextStyle(
fontSize: 35,
fontWeight: FontWeight.bold,
),
),
SizedBox(
height: 20,
),
FutureBuilder(
future: _getAllRoom(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
return Expanded(
child: TestList1(
isTablet: _isTablet,
roomModels: _roomModels,
isListStyle1: true,
));
},
),
],
),
),
);
}
// get user's models from firestore
Future _getAllRoom() async {
_roomModels.clear();
_roomModels.addAll(await RoomService().getAllRoomModel("userName"));
}
}
//
//
class TestList1 extends StatefulWidget {
final isTablet;
final List<RoomModel> roomModels;
final bool isListStyle1;
TestList1({
Key? key,
required this.isTablet,
required this.roomModels,
required this.isListStyle1,
}) : super(key: key);
#override
State<TestList1> createState() => _TestList1State();
}
class _TestList1State extends State<TestList1> {
TextEditingController _textEditingController = TextEditingController();
double _paddingSize = 40.0;
#override
void dispose() {
// _textEditingController.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return GridView.builder(
shrinkWrap: true,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: widget.isTablet ? 6 : 3,
childAspectRatio: 1 / 1.5,
mainAxisSpacing: _paddingSize,
crossAxisSpacing: 10,
),
itemBuilder: (context, index) {
return _buildMyRooms(widget.roomModels[index], index);
},
itemCount: widget.roomModels.length,
);
}
Widget _buildMyRooms(RoomModel roomModel, int index) {
return Column(
children: [
InkWell(
onTap: () {},
child: Container(
width: 120,
height: 168,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: Colors.blue,
width: 2.0,
),
borderRadius: const BorderRadius.all(
Radius.circular(10),
),
),
),
),
SizedBox(
height: sm_padding,
),
PopupMenuButton<int>(
color: Colors.grey[100],
itemBuilder: (context) => _fileMenuItemLust(roomModel),
onSelected: _onSeletedFileMenu,
child: Column(
children: [
Container(
width: 130,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flexible(
child: Text(
roomModel.roomName,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: const TextStyle(
color: Colors.blue,
fontSize: 15,
),
),
),
const Icon(
Icons.arrow_drop_down_outlined,
color: Colors.blue,
),
],
),
),
],
),
)
],
);
}
// 파일 메뉴 아이템
List<PopupMenuEntry<int>> _fileMenuItemLust(RoomModel roomModel) {
_textEditingController.text = roomModel.roomName;
return [
// 파일명
PopupMenuItem(
enabled: false,
// TODO textfield
child: TextField(
controller: _textEditingController,
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
maxLines: 1,
decoration: InputDecoration(
border: _textFieldBorder(),
enabledBorder: _textFieldBorder(),
disabledBorder: _textFieldBorder(),
focusedBorder: _textFieldBorder(),
focusColor: Colors.white60,
filled: true,
fillColor: Colors.grey.withOpacity(0.3),
isDense: true, // padding 조절을 위해 추가
contentPadding: EdgeInsets.all(sm_padding),
)),
),
const PopupMenuDivider(),
PopupMenuItem(
value: 0,
child: Row(
children: [
const Icon(
Icons.copy,
),
SizedBox(
width: sm_padding,
),
const Text(
"Menu Item1",
),
],
),
),
];
}
void _onSeletedFileMenu(value) {}
OutlineInputBorder _textFieldBorder() {
return OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(20)),
borderSide: BorderSide(
color: Colors.grey.withOpacity(0.3),
width: 1,
),
);
}
}
// ======================
I modified the code based on Yeasin Sheikh's answer.
The existing problem has been solved, but a new problem has arisen.
I'm going to update the Firestore using onSubmitted() of TextField and then update my list using stream builder and future builder.
This is a modified part of the code above to solve an existing problem
class _TestRoomState extends State<TestRoom> {
late final myFuture = _getAllRoom(); // new
FutureBuilder(
future: myFuture, //_getAllRoom(), // change
The problem of constantly rebuilding the text field according to the focus has disappeared, but there is a problem that getAllRoom() is called only at the first time and cannot be called afterwards, so the new room list cannot be updated.
Here future: _getAllRoom() calls the api on every state changes.
Create a state variable for future
late final myFuture = _getAllRoom();
#override
Widget build(BuildContext context) {
And use
FutureBuilder(
future:myFuture ,
You can check Fixing a common FutureBuilder and StreamBuilder problem
How can i show text fields based on user inputs when the button is pressed?
You can find in the screen shot i'm trying to do.
here is the code:
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
final textController = TextEditingController();
void showTextFields(){
TextField();
}
return Scaffold(
appBar: AppBar(
),
body: Center(
child: Column(
children: [
SizedBox(height: 50,),
Padding(
padding: const EdgeInsets.only(left: 20, right: 20.0),
child: TextField(
controller: textController,
decoration: InputDecoration(
labelText: 'Text fields number'
),
),
),
SizedBox(height: 10,),
ElevatedButton(
onPressed: showTextFields,
child: Text('Press to show text field')
),
],
),
),
);
}
}
I hope my help will be of use to you, greetings
class _MyHomePageState extends State<MyHomePage> {
final GlobalKey _parentKey = GlobalKey();
int countItems = 0;
late TextEditingController textController;
#override
void initState() {
super.initState();
textController = TextEditingController();
}
#override
Widget build(BuildContext context) {
void showTextFields() {
setState(() {
countItems = int.parse(textController.text);
});
}
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
children: [
const SizedBox(
height: 50,
),
Padding(
padding: const EdgeInsets.only(left: 20, right: 20.0),
child: TextField(
controller: textController,
decoration:
const InputDecoration(labelText: 'Text fields number'),
),
),
const SizedBox(
height: 10,
),
ElevatedButton(
onPressed: showTextFields,
child: const Text('Press to show text field')),
const SizedBox(
height: 10,
),
(countItems > 0)
? Flexible(
child: ListView.builder(
itemCount: countItems,
itemBuilder: (BuildContext context, int index) {
return TextField(
style: TextStyle(color: Colors.white),
cursorColor: Colors.white,
textAlign: TextAlign.center,
decoration: InputDecoration(
hintText: "TextField " + index.toString(),
hintStyle: TextStyle(color: Colors.grey[400]),
),
);
}),
)
: const SizedBox()
],
),
),
);
}
}
You can this code, Firstly you must to change stateless to StatefullWidget
and then examine to blow code
class ShowTextFieldValue extends StatefulWidget {
const ShowTextFieldValue({ Key? key }) : super(key: key);
#override
_ShowTextFieldValueState createState() => _ShowTextFieldValueState();
}
class _ShowTextFieldValueState extends State<ShowTextFieldValue> {
#override
Widget build(BuildContext context) {
final textController = TextEditingController();
String value = "";
return Scaffold(
appBar: AppBar(
),
body: Center(
child: Column(
children: [
SizedBox(height: 50,),
Padding(
padding: const EdgeInsets.only(left: 20, right: 20.0),
child: TextField(
controller: textController,
decoration: InputDecoration(
labelText: 'Text fields number'
),
),
),
SizedBox(height: 200,width: 200,
child: Center( child: Text(value)),
),
ElevatedButton(
onPressed: (){
setState(() {
value= textController.text;
});
},
child: Text('Press to show text field')
),
],
),
),
);
}
}
I'm new to flutter and I'm following this tutorial but I'm currently getting this error
error: The operator '[]' isn't defined for the type 'Map<String, dynamic> Function()'. (undefined_operator at [chat_app_tutorial] lib\views\search.dart:33)
Please I've been having this bug for over 2days now. I've checked here and I've been researching online but I've not seen anything that has worked so far. This is the line of code I'm getting the error from.
I'm not sure what part of my code is needed to be able to help me so I'll just put everything.
import 'package:chat_app_tutorial/widgets/widget.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
class SearchScreen extends StatefulWidget {
#override
_SearchScreenState createState() => _SearchScreenState();
}
class _SearchScreenState extends State<SearchScreen> {
DatabaseMethods databaseMethods = new DatabaseMethods();
TextEditingController searchTextEditingController = new TextEditingController();
QuerySnapshot searchSnapshot;
initiateSearch(){
databaseMethods.getUserByUsername(searchTextEditingController.text).then((val){
setState(() {
searchSnapshot = val;
});
});
}
Widget searchList(){
return searchSnapshot != null ? ListView.builder(
itemCount: searchSnapshot.docs.length,
shrinkWrap: true,
itemBuilder: (context, index){
return SearchTile(
userName: searchSnapshot.docs[0].data["name"],
userEmail: searchSnapshot.docs[0].data["email"],
);
}
) : Container();
}
#override
void initState() {
initiateSearch();
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: appBarMain(context),
body: SingleChildScrollView(
child: Container(
child: Column(
children: [
Container(
color: Color(0x54ffffff),
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: Row(
children: [
Expanded(
child: TextField(
controller: searchTextEditingController,
style: TextStyle(
color: Colors.white
),
decoration: InputDecoration(
hintText: "Search username",
hintStyle: TextStyle(
color: Colors.white54
),
border: InputBorder.none
),
),
),
GestureDetector(
onTap: (){
initiateSearch();
},
child: Container(
height: 40,
width: 40,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
const Color(0x36ffffff),
const Color(0x0fffffff)
]
),
borderRadius: BorderRadius.circular(40)
),
padding: EdgeInsets.all(12),
child: Image.asset("assets/images/search_white.png")
),
)
],
),
),
searchList()
],
),
),
),
);
}
}
class SearchTile extends StatelessWidget {
final String userName;
final String userEmail;
SearchTile({this.userName, this.userEmail});
#override
Widget build(BuildContext context) {
return Container(
child: Row(
children: [
Column(
children: [
Text(userName, style: simpleTextStyle(),),
Text(userEmail, style: simpleTextStyle(),)
],
),
Spacer(),
Container(
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(30)
),
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text("Message"),
)
],
),
);
}
}```
The error is complaining about:
return SearchTile(
userName: searchSnapshot.docs[0].data["name"],
userEmail: searchSnapshot.docs[0].data["email"],
);
searchSnapshot.docs[0].data is a function that returns a Map. You need to call that function first.
You probably want searchSnapshot.docs[0].data()['name'] and searchSnapshot.docs[0].data()['email'].
Even better would be to avoid calling data() multiple times:
var data = searchSnapshot.docs[0].data();
return SearchTile(userName: data['name'], userEmail: data['email']);
By definition, a container with children grows enough to show them. In this example that I am developing, I am not able to make the container fit the size of the children, I have to hardcode the Weight and Height, both, otherwise the container disappear (is the one with a red background, I put the whole code so you can copy-paste but it is only that one that I can not control the behaviour).
import 'package:flutter/material.dart';
import 'package:littlebusiness/logic/Category.dart';
import 'package:hive/hive.dart';
class FormCategoryPage extends StatefulWidget {
#override
_FormCategoryPageState createState() => _FormCategoryPageState();
}
class _FormCategoryPageState extends State<FormCategoryPage> {
final _formKey = GlobalKey<FormState>();
List<RadioModel> sampleData = new List<RadioModel>();
#override
void initState() {
// TODO: implement initState
super.initState();
sampleData.add(new RadioModel(true, 'A', 0xffe6194B));
sampleData.add(new RadioModel(false, 'B', 0xfff58231));
sampleData.add(new RadioModel(false, 'C', 0xffffe119));
sampleData.add(new RadioModel(false, 'D', 0xffbfef45));
sampleData.add(new RadioModel(true, 'A', 0xffe6194B));
sampleData.add(new RadioModel(false, 'B', 0xfff58231));
}
String _name;
Color _color;
String _selectedValue;
void addCategory(Category cat) {
Hive.box('categories').add(cat);
}
void getColor(String value) {
_selectedValue = value;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('PERFORMANCE'),
),
body: Center(
child: Form(
key: _formKey,
child: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
TextFormField(
decoration: const InputDecoration(
// hintText: 'Enter your email',
labelText: 'Name',
),
onSaved: (value) => _name = value,
validator: (value) {
if (value.isEmpty) {
return 'Please enter some text';
}
return null;
},
),
SizedBox(
height: 20,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
height: 100,
width: 320,
color: Colors.red,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: sampleData.length,
itemBuilder: (BuildContext context, int index) {
return InkWell(
onTap: () {
setState(() {
sampleData.forEach(
(element) => element.isSelected = false);
sampleData[index].isSelected = true;
});
},
child: RadioItem(sampleData[index]),
);
},
),
),
],
),
RaisedButton(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18.0),
side: BorderSide(color: Colors.red)),
child: Text('Add New Contact'),
color: Colors.teal,
textColor: Colors.white,
onPressed: () {
_formKey.currentState.save();
// final newContact = Contact(_name, int.parse(_age));
// addContact(newContact);
},
),
],
),
),
),
),
);
}
}
class RadioItem extends StatelessWidget {
final RadioModel _item;
RadioItem(this._item);
#override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.all(15.0),
child: Column(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Container(
height: 35.0,
width: 35.0,
alignment: Alignment.center,
child: Container(
height: 25.0,
width: 25.0,
decoration: BoxDecoration(
color: Color(_item.colorCode),
borderRadius:
const BorderRadius.all(const Radius.circular(15)),
)),
decoration: BoxDecoration(
color: Colors.transparent,
border: Border.all(
width: 3.0,
color: _item.isSelected
? Color(_item.colorCode)
: Colors.transparent),
borderRadius: const BorderRadius.all(const Radius.circular(25)),
),
),
Container(margin: EdgeInsets.only(left: 20.0))
],
),
);
}
}
class RadioModel {
bool isSelected;
final String buttonText;
final int colorCode;
RadioModel(this.isSelected, this.buttonText, this.colorCode);
}
This is the actuar result:
Anyone knows why it is happening that? I am lost, and giving a with of double.infinity does not work...
Thanks!
instead of using fixed values on wisth and height , you can use relative values to device by using
MediaQuery.of(context).size.width,
MediaQuery.of(context).size.height
you can also use them like
MediaQuery.of(context).size.width * 0.5
which means 50% of the device screen
hope it will help
You have to specified both width and height because your Listview is a child of a Column and a Row
You can replace your Listview by a Row of RadioItem in a SingleChildScrollView
Container(
color: Colors.red,
child: Builder(
builder: (context) {
var childs = <Widget>[];
for (var item in sampleData) {
childs.add(RadioItem(item));
}
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: childs,
),
);
},
),
),
I'm having very hard time to implement "Standard Bottom Sheet" in my application - with that I mean bottom sheet where "header" is visible and dragable (ref: https://material.io/design/components/sheets-bottom.html#standard-bottom-sheet). Even more: I can not find any example of it anywhere:S. the closes I came to wished result is by implementing DraggableScrollableSheet as bottomSheet: in Scaffold (only that widget has initialChildSize) but seams like there is no way to make a header "sticky" bc all the content is scrollable:/.
I also found this: https://flutterdoc.com/bottom-sheets-in-flutter-ec05c90453e7 - seams like there the part about "Persistent Bottom Sheet" is the one I'm looking for but artical is not detailed so I can not figure it out exacly the way to implement it plus the comments are preaty negative there so I guess it's not totally correct...
Does Anyone has any solution?:S
The standard bottom sheet behavior that you can see in the material spec can be achived using DraggableScrollableSheet.
Here I am going to explain it in detail.
Step 1:
Define your Scaffold.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Draggable sheet demo',
home: Scaffold(
///just for status bar color.
appBar: PreferredSize(
preferredSize: Size.fromHeight(0),
child: AppBar(
primary: true,
elevation: 0,
)),
body: Stack(
children: <Widget>[
Positioned(
left: 0.0,
top: 0.0,
right: 0.0,
child: PreferredSize(
preferredSize: Size.fromHeight(56.0),
child: AppBar(
title: Text("Standard bottom sheet demo"),
elevation: 2.0,
)),
),
DraggableSearchableListView(),
],
)),
);
}
}
Step 2:
Define DraggableSearchableListView
class DraggableSearchableListView extends StatefulWidget {
const DraggableSearchableListView({
Key key,
}) : super(key: key);
#override
_DraggableSearchableListViewState createState() =>
_DraggableSearchableListViewState();
}
class _DraggableSearchableListViewState
extends State<DraggableSearchableListView> {
final TextEditingController searchTextController = TextEditingController();
final ValueNotifier<bool> searchTextCloseButtonVisibility =
ValueNotifier<bool>(false);
final ValueNotifier<bool> searchFieldVisibility = ValueNotifier<bool>(false);
#override
void dispose() {
searchTextController.dispose();
searchTextCloseButtonVisibility.dispose();
searchFieldVisibility.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return NotificationListener<DraggableScrollableNotification>(
onNotification: (notification) {
if (notification.extent == 1.0) {
searchFieldVisibility.value = true;
} else {
searchFieldVisibility.value = false;
}
return true;
},
child: DraggableScrollableActuator(
child: Stack(
children: <Widget>[
DraggableScrollableSheet(
initialChildSize: 0.30,
minChildSize: 0.15,
maxChildSize: 1.0,
builder:
(BuildContext context, ScrollController scrollController) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16.0),
topRight: Radius.circular(16.0),
),
boxShadow: [
BoxShadow(
color: Colors.grey,
offset: Offset(1.0, -2.0),
blurRadius: 4.0,
spreadRadius: 2.0)
],
),
child: ListView.builder(
controller: scrollController,
///we have 25 rows plus one header row.
itemCount: 25 + 1,
itemBuilder: (BuildContext context, int index) {
if (index == 0) {
return Container(
child: Column(
children: <Widget>[
Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: EdgeInsets.only(
top: 16.0,
left: 24.0,
right: 24.0,
),
child: Text(
"Favorites",
style:
Theme.of(context).textTheme.headline6,
),
),
),
SizedBox(
height: 8.0,
),
Divider(color: Colors.grey),
],
),
);
}
return Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: ListTile(title: Text('Item $index')));
},
),
);
},
),
Positioned(
left: 0.0,
top: 0.0,
right: 0.0,
child: ValueListenableBuilder<bool>(
valueListenable: searchFieldVisibility,
builder: (context, value, child) {
return value
? PreferredSize(
preferredSize: Size.fromHeight(56.0),
child: Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
width: 1.0,
color: Theme.of(context).dividerColor),
),
color: Theme.of(context).colorScheme.surface,
),
child: SearchBar(
closeButtonVisibility:
searchTextCloseButtonVisibility,
textEditingController: searchTextController,
onClose: () {
searchFieldVisibility.value = false;
DraggableScrollableActuator.reset(context);
},
onSearchSubmit: (String value) {
///submit search query to your business logic component
},
),
),
)
: Container();
}),
),
],
),
),
);
}
}
Step 3:
Define the custom sticky SearchBar
class SearchBar extends StatelessWidget {
final TextEditingController textEditingController;
final ValueNotifier<bool> closeButtonVisibility;
final ValueChanged<String> onSearchSubmit;
final VoidCallback onClose;
const SearchBar({
Key key,
#required this.textEditingController,
#required this.closeButtonVisibility,
#required this.onSearchSubmit,
#required this.onClose,
}) : super(key: key);
#override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return Container(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 0),
child: Row(
children: <Widget>[
SizedBox(
height: 56.0,
width: 56.0,
child: Material(
type: MaterialType.transparency,
child: InkWell(
child: Icon(
Icons.arrow_back,
color: theme.textTheme.caption.color,
),
onTap: () {
FocusScope.of(context).unfocus();
textEditingController.clear();
closeButtonVisibility.value = false;
onClose();
},
),
),
),
SizedBox(
width: 16.0,
),
Expanded(
child: TextFormField(
onChanged: (value) {
if (value != null && value.length > 0) {
closeButtonVisibility.value = true;
} else {
closeButtonVisibility.value = false;
}
},
onFieldSubmitted: (value) {
FocusScope.of(context).unfocus();
onSearchSubmit(value);
},
keyboardType: TextInputType.text,
textInputAction: TextInputAction.search,
textCapitalization: TextCapitalization.none,
textAlignVertical: TextAlignVertical.center,
textAlign: TextAlign.left,
maxLines: 1,
controller: textEditingController,
decoration: InputDecoration(
isDense: true,
border: InputBorder.none,
hintText: "Search here",
),
),
),
ValueListenableBuilder<bool>(
valueListenable: closeButtonVisibility,
builder: (context, value, child) {
return value
? SizedBox(
width: 56.0,
height: 56.0,
child: Material(
type: MaterialType.transparency,
child: InkWell(
child: Icon(
Icons.close,
color: theme.textTheme.caption.color,
),
onTap: () {
closeButtonVisibility.value = false;
textEditingController.clear();
},
),
),
)
: Container();
})
],
),
),
);
}
}
See the screenshots of the final output.
state 1:
The bottom sheet is shown with it's initial size.
state 2:
User dragged up the bottom sheet.
state 3:
The bottom sheet reached the top edge of the screen and a sticky custom SearchBar interface is shown.
That's all.
See the live demo here.
As #Sergio named some good alternatives it still needs more coding to make it work as it should with that said, I found Sliding_up_panel so for anyone else looking for solution You can find it here .
Still, I find it really weird that built in bottomSheet widget in Flutter does not provide options for creating "standard bottom sheet" mentioned in material.io :S
If you are looking for Persistent Bottomsheet than please refer the source code from below link
Persistent Bottomsheet
You can refer the _showBottomSheet() for your requirement and some changes will fulfil your requirement
You can do it using a stack and an animation:
class HelloWorldPage extends StatefulWidget {
#override
_HelloWorldPageState createState() => _HelloWorldPageState();
}
class _HelloWorldPageState extends State<HelloWorldPage>
with SingleTickerProviderStateMixin {
final double minSize = 80;
final double maxSize = 350;
void initState() {
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 500))
..addListener(() {
setState(() {});
});
_animation =
Tween<double>(begin: minSize, end: maxSize).animate(_controller);
super.initState();
}
AnimationController _controller;
Animation<double> _animation;
#override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
fit: StackFit.expand,
children: <Widget>[
Positioned(
bottom: 0,
height: _animation.value,
child: GestureDetector(
onDoubleTap: () => _onEvent(),
onVerticalDragEnd: (event) => _onEvent(),
child: Container(
color: Colors.red,
width: MediaQuery.of(context).size.width,
height: minSize,
),
),
),
],
),
);
}
_onEvent() {
if (_controller.isCompleted) {
_controller.reverse(from: maxSize);
} else {
_controller.forward();
}
}
#override
void dispose() {
_controller.dispose();
super.dispose();
}
}
Can easily be achieved with showModalBottomSheet. Code:
void _presentBottomSheet(BuildContext context) {
showModalBottomSheet(
context: context,
builder: (context) => Wrap(
children: <Widget>[
SizedBox(height: 8),
_buildBottomSheetRow(context, Icons.share, 'Share'),
_buildBottomSheetRow(context, Icons.link, 'Get link'),
_buildBottomSheetRow(context, Icons.edit, 'Edit Name'),
_buildBottomSheetRow(context, Icons.delete, 'Delete collection'),
],
),
);
}
Widget _buildBottomSheetRow(
BuildContext context,
IconData icon,
String text,
) =>
InkWell(
onTap: () {},
child: Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(16),
child: Icon(
icon,
color: Colors.grey[700],
),
),
SizedBox(width: 8),
Text(text),
],
),
);