Filtering a ListView in Flutter using a onChange on TextField with delay in flutter.

By | November 9, 2019

In this article we will see how we can filter a Listview in the onChange event of a TextField in Flutter.

Here we will use the below service Url to populate data into the ListView.

https://jsonplaceholder.typicode.com/users

Filtering in ListView Flutter


Watch Video Tutorial


Create Model Class

class User {
  int id;
  String name;
  String email;

  User({this.id, this.name, this.email});

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json["id"] as int,
      name: json["name"] as String,
      email: json["email"] as String,
    );
  }
}

Service Class

In this class we will call the service and get the data. Then we will parse the data to get the list of users.

import 'dart:convert';
import 'package:http/http.dart' as http;
import 'User.dart';

class Services {
  static const String url = 'https://jsonplaceholder.typicode.com/users';

  static Future<List<User>> getUsers() async {
    try {
      final response = await http.get(url);
      if (response.statusCode == 200) {
        List<User> list = parseUsers(response.body);
        return list;
      } else {
        throw Exception("Error");
      }
    } catch (e) {
      throw Exception(e.toString());
    }
  }

  static List<User> parseUsers(String responseBody) {
    final parsed = json.decode(responseBody).cast<Map<String, dynamic>>();
    return parsed.map<User>((json) => User.fromJson(json)).toList();
  }
}

So now we have the list of users from the service.


Show the List

Lets create a new variable in the Main file.

  List<User> users = List();

Override the initState method and initialize the variable.

@override
void initState() {
  super.initState();
  Services.getUsers().then((usersFromServer) {
    setState(() {
      users = usersFromServer;
    });
  });
}

// in the build method
Expanded(
  child: ListView.builder(
    padding: EdgeInsets.all(10.0),
    itemCount: users.length,
    itemBuilder: (BuildContext context, int index) {
      return Card(
        child: Padding(
          padding: EdgeInsets.all(10.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.start,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Text(
                users[index].name,
                style: TextStyle(
                  fontSize: 16.0,
                  color: Colors.black,
                ),
              ),
              SizedBox(
                height: 5.0,
              ),
              Text(
                users[index].email.toLowerCase(),
                style: TextStyle(
                  fontSize: 14.0,
                  color: Colors.grey,
                ),
              ),
            ],
          ),
        ),
      );
    },
  ),
),

Add the Search TextField


 List<User> filteredUsers = List();

 // in the build method above list
 TextField(
  decoration: InputDecoration(
    contentPadding: EdgeInsets.all(15.0),
    hintText: 'Filter by name or email',
  ),
  onChanged: (string) {
      setState(() {
        filteredUsers = users
            .where((u) => (u.name
                    .toLowerCase()
                    .contains(string.toLowerCase()) ||
                u.email.toLowerCase().contains(string.toLowerCase())))
            .toList();
      });
   
  },
),

Update the List

Noe update the list with filteredUsers as the source.

Expanded(
  child: ListView.builder(
    padding: EdgeInsets.all(10.0),
    itemCount: filteredUsers.length,
    itemBuilder: (BuildContext context, int index) {
      return Card(
        child: Padding(
          padding: EdgeInsets.all(10.0),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.start,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Text(
                filteredUsers[index].name,
                style: TextStyle(
                  fontSize: 16.0,
                  color: Colors.black,
                ),
              ),
              SizedBox(
                height: 5.0,
              ),
              Text(
                filteredUsers[index].email.toLowerCase(),
                style: TextStyle(
                  fontSize: 14.0,
                  color: Colors.grey,
                ),
              ),
            ],
          ),
        ),
      );
    },
  ),
),

Adding Wait for searching

We don’t want to search on every keystroke in the TextField, so we will write a debouncer and wait for certain time to search after the user types.
so create a class named “debouncer” and add the following code.

class Debouncer {
  final int milliseconds;
  VoidCallback action;
  Timer _timer;

  Debouncer({this.milliseconds});

  run(VoidCallback action) {
    if (null != _timer) {
      _timer.cancel();
    }
    _timer = Timer(Duration(milliseconds: milliseconds), action);
  }
}

So Create a new variable

final _debouncer = Debouncer(milliseconds: 500);

Here we are giving a delay of 500 milliseconds, that means it will start
searching after 500 milliseconds the user stops typing. we are actually adding a timer that cancels every time the user types and starts when he stops typing and the above timer will fire after 500 milliseconds.

Now the onChange callback for the TextField will change like below.

 onChanged: (string) {
  _debouncer.run(() {
    setState(() {
      filteredUsers = users
          .where((u) => (u.name
                  .toLowerCase()
                  .contains(string.toLowerCase()) ||
              u.email.toLowerCase().contains(string.toLowerCase())))
          .toList();
    });
  });
},

Complete Source code

import 'package:flutter/material.dart';
import 'dart:async';
import 'User.dart';
import 'Services.dart';

class UserFilterDemo extends StatefulWidget {
  UserFilterDemo() : super();

  final String title = "Filter List Demo";

  @override
  UserFilterDemoState createState() => UserFilterDemoState();
}

class Debouncer {
  final int milliseconds;
  VoidCallback action;
  Timer _timer;

  Debouncer({this.milliseconds});

  run(VoidCallback action) {
    if (null != _timer) {
      _timer.cancel();
    }
    _timer = Timer(Duration(milliseconds: milliseconds), action);
  }
}

class UserFilterDemoState extends State<UserFilterDemo> {
  // https://jsonplaceholder.typicode.com/users

  final _debouncer = Debouncer(milliseconds: 500);
  List<User> users = List();
  List<User> filteredUsers = List();

  @override
  void initState() {
    super.initState();
    Services.getUsers().then((usersFromServer) {
      setState(() {
        users = usersFromServer;
        filteredUsers = users;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Column(
        children: <Widget>[
          TextField(
            decoration: InputDecoration(
              contentPadding: EdgeInsets.all(15.0),
              hintText: 'Filter by name or email',
            ),
            onChanged: (string) {
              _debouncer.run(() {
                setState(() {
                  filteredUsers = users
                      .where((u) => (u.name
                              .toLowerCase()
                              .contains(string.toLowerCase()) ||
                          u.email.toLowerCase().contains(string.toLowerCase())))
                      .toList();
                });
              });
            },
          ),
          Expanded(
            child: ListView.builder(
              padding: EdgeInsets.all(10.0),
              itemCount: filteredUsers.length,
              itemBuilder: (BuildContext context, int index) {
                return Card(
                  child: Padding(
                    padding: EdgeInsets.all(10.0),
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.start,
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: <Widget>[
                        Text(
                          filteredUsers[index].name,
                          style: TextStyle(
                            fontSize: 16.0,
                            color: Colors.black,
                          ),
                        ),
                        SizedBox(
                          height: 5.0,
                        ),
                        Text(
                          filteredUsers[index].email.toLowerCase(),
                          style: TextStyle(
                            fontSize: 14.0,
                            color: Colors.grey,
                          ),
                        ),
                      ],
                    ),
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

Source code github link: https://bitbucket.org/vipinvijayan1987/tutorialprojects/src/master/FlutterTutorialProjects/flutter_demo1/lib/widgets/List/


6 thoughts on “Filtering a ListView in Flutter using a onChange on TextField with delay in flutter.

  1. ozan k

    Thank you bro for your open source code 🙂 it was so helpful!

    Reply
  2. Dragon Fly

    Would you please post a tutorial for filtering list of two line, but when the data list source is the app itself (internal). Thank you

    Reply
  3. User

    Hello everyone
    I start flutter journey and after 3 days trying to fetch data from an api, I am asking your help

    I have this kind of data.

    {
    ….

    “name”: “Apple”
    ….

    }

    Here’s the api : https://finnhub.io/api/v1/stock/profile2?symbol=AAPL&token=

    I have followed several tutorials but only this one works a bit : https://youtu.be/-EUsRa2G1zk

    Unfortunately the data type in this tutorial is like

    [

    {
    ….
    “name” : “Apple”
    ….
    }

    ]

    So nothing works with my first api

    Could anyone help?

    I want to do a listview with a TextField able to retrieve information from an api when user tap (exemple : AAPL)

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *