Listview filter search in Flutter

115,634

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):

enter image description here


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.

Share:
115,634
heyr
Author by

heyr

Full-stack Developer

Updated on July 09, 2022

Comments

  • heyr
    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 a z according to my case, I should not display anything in the List. 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);
       }
       }
    

    This is how my interface looks like right now

  • heyr
    heyr almost 6 years
    Thank you! You definitely gave me a crystal clear example!
  • Vinoth Kumar
    Vinoth Kumar almost 6 years
    You're welcome. I've also learned a new thing which I haven't done before because of your question.
  • heyr
    heyr almost 6 years
    I 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
    Vinoth Kumar almost 6 years
    Can you post the userdetails list so that it would be helpful to find the issue.
  • Vinoth Kumar
    Vinoth Kumar almost 6 years
    Since 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
    heyr almost 6 years
    Sure. print(userDetails) => {first_name: Test, last_name: Account, profile_image: tempURL}} . print(returnTicketDetails) => user_id: 513549601, event_id: 843678542, code: TKHRDSJ,
  • Vinoth Kumar
    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
    heyr almost 6 years
    Much 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
    Vinoth Kumar almost 6 years
    Take a loot at my new answer.
  • heyr
    heyr almost 6 years
    I just fixed it! Apparently I missed out an initState. Thank you though!
  • fvisticot
    fvisticot over 5 years
    When 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
    Vinoth Kumar over 5 years
    You can use 'resizeToAvoidBottomPadding' property of Scaffold like 'resizeToAvoidBottomPadding: false,'
  • Pawan
    Pawan over 5 years
    Thanks @Vinoth Kumar. You should write 1 whole tutorial on this, Yours is the best explanation. Helped a lot.
  • Melvin Abraham
    Melvin Abraham over 5 years
    @VinothKumar Thanks for referring to Expanded Widget... That Widget saved my day. ;D
  • cdsaenz
    cdsaenz over 4 years
    Thanks 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.toUpperCas‌​e()) || userDetail.lastName.toUpperCase().contains(text.toUpperCase(‌​))) _searchResult.add(userDetail); });
  • ABHIMANGAL MS
    ABHIMANGAL MS over 4 years
    In the first search itself I found this code. Thank you very much.
  • Ujjwal Raijada
    Ujjwal Raijada about 3 years
    I found "" .toLowerCase() "" to be very important. Thanks