How is the search bar, shown in the two attached screenshots, implemented? There is a map on the page, but when I click on the search bar, it displays the search field along with the search results, without navigating to a new page. The BottomNavigationBar
remains visible, and the filter buttons (Pavilony, WC, etc.) disappear. I initially thought this was a SearchDelegate
, but that would load an entirely new page. How is this behavior achieved?
If you want to build a Custom searching by searching and get the result at the same screen you can implement this by building the UI and the management of the searching from search
=> Demo video for the provided Solution : CustomSearching
I had a solution for the mechanism you are want to achieve
CustomAppBarWidget
class CustomAppBarWidget extends StatelessWidget {
const CustomAppBarWidget({super.key});
@override
Widget build(BuildContext context) {
return Container(
height: MediaQuery.sizeOf(context).height * .16,
decoration: BoxDecoration(
color: Colors.orange,
borderRadius: BorderRadius.circular(20),
),
child: Consumer<ManageSearch>(
builder: (context, search, _) {
return Column(
children: <Widget>[
const SizedBox(height: 40),
Container(
margin: const EdgeInsets.all(10),
child: TextField(
controller: ManageSearch.searchController,
onChanged: (target) {
search.searchCity;
},
decoration: InputDecoration(
filled: true,
fillColor: Colors.white,
hintText: "Search",
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15.0),
borderSide: const BorderSide(
color: Colors.grey,
width: 1.0,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15.0),
borderSide: const BorderSide(
color: Colors.grey,
width: 1.5,
),
),
),
),
),
],
);
},
),
);
}
}
DemoNavigationWidget
just to simulate your UI
class NavigationModel {
final String label;
final IconData icon;
NavigationModel({
required this.icon,
required this.label,
});
}
List<NavigationModel> navigations = <NavigationModel>[
NavigationModel(icon: Icons.home, label: "Home"),
NavigationModel(icon: Icons.person, label: "Profile"),
NavigationModel(icon: Icons.settings, label: "Settings"),
NavigationModel(icon: Icons.map, label: "Map"),
];
class DemoNavigationBar extends StatelessWidget {
const DemoNavigationBar({super.key});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(25),
color: Colors.white,
),
margin: const EdgeInsets.symmetric(horizontal: 20),
padding: const EdgeInsets.all(10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
for (int i = 0; i < navigations.length; i++) ...{
Column(
children: <Widget>[
Icon(navigations[i].icon),
Text(navigations[i].label)
],
),
}
],
),
);
}
}
DemoFilterWidget
also just to simulate your UI// Demo Filter
List<ItemModel> filters = <ItemModel>[
ItemModel(icon: Icons.wc, label: "WC"),
ItemModel(icon: Icons.privacy_tip, label: "PR"),
ItemModel(icon: Icons.restaurant, label: "rest"),
ItemModel(icon: Icons.data_thresholding, label: "Data"),
];
class DemoFilter extends StatelessWidget {
const DemoFilter({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
height: MediaQuery.sizeOf(context).height * .08,
child: ListView.builder(
itemCount: filters.length,
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return Container(
width: MediaQuery.sizeOf(context).width * .25,
padding: const EdgeInsets.all(10),
margin: const EdgeInsets.all(10),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
color: const Color(0xFFE7FBE6),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(filters[index].icon),
Text(filters[index].label),
],
),
);
},
),
);
}
}
List<String> cities = <String>[
"Tokyo",
"Delhi",
"Shanghai",
"São Paulo",
"Mexico City",
"Cairo",
"Mumbai",
"Beijing",
"Dhaka",
"Osaka",
"New York",
"Karachi",
"Buenos Aires",
"Chongqing",
"Istanbul",
"Kolkata",
"Manila",
"Lagos",
"Rio de Janeiro",
"Tianjin",
"Kinshasa",
"Guangzhou",
"Los Angeles",
"Moscow",
"Shenzhen",
"Lahore",
"Bangalore",
"Paris",
"Bogotá",
"Jakarta"
];
class ManageSearch with ChangeNotifier {
List<String> results = <String>[];
static TextEditingController? searchController;
void get searchCity {
String target = searchController!.text;
results = cities.where(
(city) {
bool isExist = city.toLowerCase().trim().contains(
target.toLowerCase().trim(),
);
return isExist;
},
).toList();
notifyListeners();
}
bool get startSearch {
bool isUserSearch = searchController!.text.isNotEmpty;
return isUserSearch;
}
bool get isEmptyResults {
bool isResultEmpty = results.isEmpty;
bool isEmpty = isResultEmpty && startSearch;
return isEmpty;
}
bool get hasResult {
bool hasResult = results.isNotEmpty && startSearch;
return hasResult;
}
}
MainScreen
@override
void initState() {
ManageSearch.searchController = TextEditingController();
super.initState();
}
@override
void dispose() {
ManageSearch.searchController!.dispose();
super.dispose();
}
MainScreen
and its components
class CustomSearchScreen extends StatefulWidget {
const CustomSearchScreen({super.key});
@override
State<CustomSearchScreen> createState() => _CustomSearchScreenState();
}
class _CustomSearchScreenState extends State<CustomSearchScreen> {
@override
void initState() {
ManageSearch.searchController = TextEditingController();
super.initState();
}
@override
void dispose() {
ManageSearch.searchController!.dispose();
super.dispose();
}
double get height => MediaQuery.sizeOf(context).height;
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => ManageSearch(),
child: Consumer<ManageSearch>(builder: (context, search, _) {
return Scaffold(
resizeToAvoidBottomInset: false,
body: Stack(
alignment: Alignment.center,
children: <Widget>[
if (search.isEmptyResults) ...{
Positioned.fill(
top: height * .17,
child: const Center(
child: Text(
"Empty Results",
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.red,
),
),
),
),
} else ...{
Positioned.fill(
top: height * .15,
child: search.startSearch
? ListView.builder(
padding: const EdgeInsets.only(top: 20),
itemCount: search.results.length,
itemBuilder: (context, index) {
return Container(
padding: const EdgeInsets.all(10),
margin: const EdgeInsets.all(15.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.white,
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
search.results[index],
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.bold,
),
),
const Icon(Icons.location_on)
],
),
);
},
)
: InteractiveViewer(
child: Image.asset(
"assets/images/others/profile/test_map.jpg",
fit: BoxFit.cover,
),
),
),
},
const Positioned(
top: 0.0,
right: 0.0,
left: 0.0,
child: CustomAppBarWidget(),
),
if (!search.startSearch) ...{
Positioned(
top: height * .14,
right: 0.0,
left: 0.0,
child: const DemoFilter(),
),
},
Positioned(
right: 0.0,
left: 0.0,
bottom: 0.0,
child: Container(
height: height * .08,
decoration: const BoxDecoration(
borderRadius: BorderRadius.vertical(
top: Radius.circular(25),
),
color: Colors.orange,
),
),
),
Positioned(
bottom: height * .03,
right: 0.0,
left: 0.0,
child: const DemoNavigationBar(),
),
],
),
);
}),
);
}
}
If you want you can use the
setState((){})
instead theProvider
but I used the Provider to build it cleanYou can control everything with the
bool
s as Searching Status