Listview filter search in Flutter
Solution 1
I've replaced hardcoded model input with getting data from URL as you needed.
import 'dart:async';
import 'package:flutter/material.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
void main() => runApp(new MaterialApp(
home: new HomePage(),
debugShowCheckedModeBanner: false,
));
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => new _HomePageState();
}
class _HomePageState extends State<HomePage> {
TextEditingController controller = new TextEditingController();
// Get json result and convert it to model. Then add
Future<Null> getUserDetails() async {
final response = await http.get(url);
final responseJson = json.decode(response.body);
setState(() {
for (Map user in responseJson) {
_userDetails.add(UserDetails.fromJson(user));
}
});
}
@override
void initState() {
super.initState();
getUserDetails();
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Home'),
elevation: 0.0,
),
body: new Column(
children: <Widget>[
new Container(
color: Theme.of(context).primaryColor,
child: new Padding(
padding: const EdgeInsets.all(8.0),
child: new Card(
child: new ListTile(
leading: new Icon(Icons.search),
title: new TextField(
controller: controller,
decoration: new InputDecoration(
hintText: 'Search', border: InputBorder.none),
onChanged: onSearchTextChanged,
),
trailing: new IconButton(icon: new Icon(Icons.cancel), onPressed: () {
controller.clear();
onSearchTextChanged('');
},),
),
),
),
),
new Expanded(
child: _searchResult.length != 0 || controller.text.isNotEmpty
? new ListView.builder(
itemCount: _searchResult.length,
itemBuilder: (context, i) {
return new Card(
child: new ListTile(
leading: new CircleAvatar(backgroundImage: new NetworkImage(_searchResult[i].profileUrl,),),
title: new Text(_searchResult[i].firstName + ' ' + _searchResult[i].lastName),
),
margin: const EdgeInsets.all(0.0),
);
},
)
: new ListView.builder(
itemCount: _userDetails.length,
itemBuilder: (context, index) {
return new Card(
child: new ListTile(
leading: new CircleAvatar(backgroundImage: new NetworkImage(_userDetails[index].profileUrl,),),
title: new Text(_userDetails[index].firstName + ' ' + _userDetails[index].lastName),
),
margin: const EdgeInsets.all(0.0),
);
},
),
),
],
),
);
}
onSearchTextChanged(String text) async {
_searchResult.clear();
if (text.isEmpty) {
setState(() {});
return;
}
_userDetails.forEach((userDetail) {
if (userDetail.firstName.contains(text) || userDetail.lastName.contains(text))
_searchResult.add(userDetail);
});
setState(() {});
}
}
List<UserDetails> _searchResult = [];
List<UserDetails> _userDetails = [];
final String url = 'https://jsonplaceholder.typicode.com/users';
class UserDetails {
final int id;
final String firstName, lastName, profileUrl;
UserDetails({this.id, this.firstName, this.lastName, this.profileUrl = 'https://i.amz.mshcdn.com/3NbrfEiECotKyhcUhgPJHbrL7zM=/950x534/filters:quality(90)/2014%2F06%2F02%2Fc0%2Fzuckheadsho.a33d0.jpg'});
factory UserDetails.fromJson(Map<String, dynamic> json) {
return new UserDetails(
id: json['id'],
firstName: json['name'],
lastName: json['username'],
);
}
}
Solution 2
I'm learning Flutter and was looking for a searchable list which @Vinoth Kumar example does perfectly.
I've split the code into different files and reduced the HomePage body into several methods to make it more maintainable/readable for myself and I figured it would be worth sharing.
main.dart
import 'package:flutter/material.dart';
import 'homepage.dart';
void main() => runApp(new MaterialApp(
home: new HomePage(),
debugShowCheckedModeBanner: false,
));
homepage.dart
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';
import 'userDetails.dart';
class HomePage extends StatefulWidget {
HomePage({Key key}) : super(key: key);
@override
_HomePageState createState() => new _HomePageState();
}
class _HomePageState extends State<HomePage> {
List<UserDetails> _searchResult = [];
List<UserDetails> _userDetails = [];
TextEditingController controller = new TextEditingController();
// Get json result and convert it to model. Then add
Future<Null> getUserDetails() async {
final response = await http.get(url);
final responseJson = json.decode(response.body);
setState(() {
for (Map user in responseJson) {
_userDetails.add(UserDetails.fromJson(user));
}
});
}
@override
void initState() {
super.initState();
getUserDetails();
}
Widget _buildUsersList() {
return new ListView.builder(
itemCount: _userDetails.length,
itemBuilder: (context, index) {
return new Card(
child: new ListTile(
leading: new CircleAvatar(
backgroundImage: new NetworkImage(
_userDetails[index].profileUrl,
),
),
title: new Text(_userDetails[index].firstName +
' ' +
_userDetails[index].lastName),
),
margin: const EdgeInsets.all(0.0),
);
},
);
}
Widget _buildSearchResults() {
return new ListView.builder(
itemCount: _searchResult.length,
itemBuilder: (context, i) {
return new Card(
child: new ListTile(
leading: new CircleAvatar(
backgroundImage: new NetworkImage(
_searchResult[i].profileUrl,
),
),
title: new Text(
_searchResult[i].firstName + ' ' +_searchResult[i].lastName),
),
margin: const EdgeInsets.all(0.0),
);
},
);
}
Widget _buildSearchBox() {
return new Padding(
padding: const EdgeInsets.all(8.0),
child: new Card(
child: new ListTile(
leading: new Icon(Icons.search),
title: new TextField(
controller: controller,
decoration: new InputDecoration(
hintText: 'Search', border: InputBorder.none),
onChanged: onSearchTextChanged,
),
trailing: new IconButton(
icon: new Icon(Icons.cancel),
onPressed: () {
controller.clear();
onSearchTextChanged('');
},
),
),
),
);
}
Widget _buildBody() {
return new Column(
children: <Widget>[
new Container(
color: Theme.of(context).primaryColor, child: _buildSearchBox()),
new Expanded(
child: _searchResult.length != 0 || controller.text.isNotEmpty
? _buildSearchResults()
: _buildUsersList()),
],
);
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Home'),
elevation: 0.0,
),
body: _buildBody(),
resizeToAvoidBottomPadding: true,
);
}
onSearchTextChanged(String text) async {
_searchResult.clear();
if (text.isEmpty) {
setState(() {});
return;
}
_userDetails.forEach((userDetail) {
if (userDetail.firstName.contains(text) ||
userDetail.lastName.contains(text)) _searchResult.add(userDetail);
});
setState(() {});
}
}
userDetails.dart
import 'package:flutter/material.dart';
final String url = 'https://jsonplaceholder.typicode.com/users';
class UserDetails {
final int id;
final String firstName, lastName, profileUrl;
UserDetails({this.id, this.firstName, this.lastName, this.profileUrl = 'https://i.amz.mshcdn.com/3NbrfEiECotKyhcUhgPJHbrL7zM=/950x534/filters:quality(90)/2014%2F06%2F02%2Fc0%2Fzuckheadsho.a33d0.jpg'});
factory UserDetails.fromJson(Map<String, dynamic> json) {
return new UserDetails(
id: json['id'],
firstName: json['name'],
lastName: json['username'],
);
}
}
Solution 3
Use SearchDelegate
(Null Safe):
Full code:
class SearchPage extends StatefulWidget {
@override
_SearchPageState createState() => _SearchPageState();
}
class _SearchPageState extends State<SearchPage> {
String? _result;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Search')),
body: Center(
child: Column(
children: <Widget>[
Text(_result ?? '', style: TextStyle(fontSize: 18)),
ElevatedButton(
onPressed: () async {
var result = await showSearch<String>(
context: context,
delegate: CustomDelegate(),
);
setState(() => _result = result);
},
child: Text('Search'),
),
],
),
),
);
}
}
class CustomDelegate extends SearchDelegate<String> {
List<String> data = nouns.take(100).toList();
@override
List<Widget> buildActions(BuildContext context) => [IconButton(icon: Icon(Icons.clear), onPressed: () => query = '')];
@override
Widget buildLeading(BuildContext context) => IconButton(icon: Icon(Icons.chevron_left), onPressed: () => close(context, ''));
@override
Widget buildResults(BuildContext context) => Container();
@override
Widget buildSuggestions(BuildContext context) {
var listToShow;
if (query.isNotEmpty)
listToShow = data.where((e) => e.contains(query) && e.startsWith(query)).toList();
else
listToShow = data;
return ListView.builder(
itemCount: listToShow.length,
itemBuilder: (_, i) {
var noun = listToShow[i];
return ListTile(
title: Text(noun),
onTap: () => close(context, noun),
);
},
);
}
}
Solution 4
In Flutter, we have to manage with a custom filter widget and we have to compare tow different objects lists. Such as
if (country.name.toLowerCase().contains(searchQuery) ||
country.name.contains(searchQuery)) {
filteredRecored.add(country);
}
I found example here
Solution 5
With reference to Vinoth Kumar's answer, it's good to note that .contains()
is case-sensitive. So, to exhaust all matches, you could transform the strings to lowercase:
_userDetails.forEach((userDetail) {
if (userDetail.firstName.toLowerCase().contains(text.toLowerCase()) || userDetail.lastName.toLowerCase().contains(text.toLowerCase()))
_searchResult.add(userDetail);
});
Works perfectly for me.
Comments
-
heyr almost 2 years
I was wondering how to make the search function works according to the index of my
ListView
? So for instance if I input az
according to my case, I should not display anything in theList
. I have also updated and posted the function_getTicketDeatils()
here.var userDetails = {}; var i; List returnTicketDetails = [] ; body: new Column( children: <Widget>[ new Container( color: Theme.of(context).primaryColor, child: new Padding( padding: const EdgeInsets.all(8.0), child: new Card( child: new ListTile( leading: new Icon(Icons.search), title: new TextField( controller: controller, decoration: new InputDecoration( hintText: 'Search', border: InputBorder.none), // onChanged: onSearchTextChanged, ), trailing: new IconButton(icon: new Icon(Icons.cancel), onPressed: () { controller.clear(); // onSearchTextChanged(''); },), ), new Expanded( child: userDetails.length != 0 || controller.text.isNotEmpty ? new ListView.builder( itemCount: userDetails.length, itemBuilder: (context, i) { return new Card( child: new Column (mainAxisSize: MainAxisSize.min, children: <Widget>[ new Row(children: <Widget>[ new Container( width: 80.0, height: 80.0, decoration: new BoxDecoration( shape: BoxShape.circle, image: new DecorationImage( fit: BoxFit.fill, image: new NetworkImage( "https:..") ) )), new Text(userDetails[returnTicketDetails[i]["user_id"]]["first_name"] ),), ,), new Text(userDetails[returnTicketDetails[i]["user_id"]]["last_name"]), ); }, ) : new ListView.builder( itemCount: userDetails.length, itemBuilder: (context, i) { return new Card( child: new ListTile( //title: new Text(userDetails[returnTicketDetails[i]["user_id"]]["first_name"]), ), margin: const EdgeInsets.all(0.0), ); ); } _getTicketDetails() async { final response = await http.get( "https..", headers: { HttpHeaders.AUTHORIZATION: access_token }); returnTicketDetails = json.decode(response.body); for ( i = 0; i < (returnTicketDetails?.length ?? 0); i++) { final ticketresponse = await http.get( "https...", headers: { HttpHeaders.AUTHORIZATION: access_token }); userDetails[returnTicketDetails[i]["user_id"]] = json.decode(ticketresponse.body); } }
-
heyr almost 6 yearsThank you! You definitely gave me a crystal clear example!
-
Vinoth Kumar almost 6 yearsYou're welcome. I've also learned a new thing which I haven't done before because of your question.
-
heyr almost 6 yearsI have updated the post with the outcome obtained, however the search does not work as I did not assign string in the same way as you did on your example, do you mind to have a look at it please? Thank you again
-
Vinoth Kumar almost 6 yearsCan you post the userdetails list so that it would be helpful to find the issue.
-
Vinoth Kumar almost 6 yearsSince I won't be able to access the URL can you please give me a sample data for the two variables. Even a single data is enough for me.
-
heyr almost 6 yearsSure. print(userDetails) => {first_name: Test, last_name: Account, profile_image: tempURL}} . print(returnTicketDetails) => user_id: 513549601, event_id: 843678542, code: TKHRDSJ,
-
Vinoth Kumar almost 6 years@HeyabRedda I've edited my answer based on your needs. Check it out and remind me if you need to change anything in my answer or if you don't understand any of it.
-
heyr almost 6 yearsMuch appreciated for your effort, however I wanted to clarify that the card gets created dynamically since I send API request, hence the List userdetails cannot really be hardcoded as you did. @Vinoth Kuma
-
Vinoth Kumar almost 6 yearsTake a loot at my new answer.
-
heyr almost 6 yearsI just fixed it! Apparently I missed out an initState. Thank you though!
-
fvisticot over 5 yearsWhen I start a search the keyboard pop up and the list view scrolls up. How to avoid to have the list scroll up ?
-
Vinoth Kumar over 5 yearsYou can use 'resizeToAvoidBottomPadding' property of Scaffold like 'resizeToAvoidBottomPadding: false,'
-
Pawan over 5 yearsThanks @Vinoth Kumar. You should write 1 whole tutorial on this, Yours is the best explanation. Helped a lot.
-
Melvin Abraham over 5 years@VinothKumar Thanks for referring to
Expanded
Widget... That Widget saved my day.;D
-
cdsaenz over 4 yearsThanks for the complete working code, works perfectly. Changed this to have a non sensitive search (seems to work!):
_userDetails.forEach((userDetail) { if (userDetail.firstName.toUpperCase().contains(text.toUpperCase()) || userDetail.lastName.toUpperCase().contains(text.toUpperCase())) _searchResult.add(userDetail); });
-
ABHIMANGAL MS over 4 yearsIn the first search itself I found this code. Thank you very much.
-
Ujjwal Raijada about 3 yearsI found "" .toLowerCase() "" to be very important. Thanks