Listview filter search in Flutter
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'],
);
}
}
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),
);
},
);
}
}
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'],
);
}
}