I want to filter a list view by text entered into a search field. Many examples online and on this site, but all over-simplified with everything in a single stateful widget and/or seem a bit messy and maybe not the best way to structure things. I have a simple app that uses Provider, a model class (Dog) and a Dogs class that has my list I'm working with.

Goal: Filter the list of dogs by text entered.

Dog model

class Dog {
  final String breed;
  final String name;
  final int age;

  Dog({this.breed, this.name, this.age});
}

Dogs class

import 'package:flutter/cupertino.dart';
import 'dart:collection';

import '../models/dog.dart';

class Dogs extends ChangeNotifier {
  final List<Dog> _myDogs = [
    Dog(name: 'Mackie', age: 8, breed: 'Rottweiler'),
    Dog(name: 'Riley', age: 8, breed: 'Rottweiler'),
    Dog(name: 'Tank', age: 7, breed: 'Mastiff'),
    Dog(name: 'Tanner', age: 7, breed: 'Mastiff'),
    Dog(name: 'Rocky', age: 10, breed: 'Rottweiler'),
    Dog(name: 'Angel', age: 11, breed: 'Poodle'),
    Dog(name: 'Roxie', age: 8, breed: 'St. Bernard'),
    Dog(name: 'Spud', age: 8, breed: 'St. Bernard'),
    Dog(name: 'Nick', age: 4, breed: 'Rottweiler'),
  ];

  UnmodifiableListView<Dog> get dogs => UnmodifiableListView(_myDogs);
}

Main with ListView

import 'package:flutter/material.dart';

import 'package:provider/provider.dart';
import 'providers/dogs.dart';

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => Dogs(),
      child: MyApp(),
    ),
  );
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final _controller = TextEditingController();
  String _searchText;

  @override
  void initState() {
    _controller.addListener((){
      setState(() {
        _searchText = _controller.text;
      });
    },);
    super.initState();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Dogs',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Dogs'),
        ),
        body: Column(
          mainAxisAlignment: MainAxisAlignment.start,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            TextField(
              controller: _controller,
              decoration: InputDecoration(
                hintText: "Search",
                prefixIcon: Icon(Icons.search),
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.all(
                    Radius.circular(4.0),
                  ),
                ),
              ),
              onChanged: (value){
                //TODO when text is entered into search bar
              },
            ),
            Consumer<Dogs>(
              builder: (context, dogData, child) => Expanded(
                child: ListView.builder(
                    shrinkWrap: true,
                    itemCount: dogData.dogs.length,
                    itemBuilder: (context, index) => Card(
                          elevation: 3,
                          child: ListTile(
                            title: Text(dogData.dogs[index].name),
                          ),
                        )),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

After several iterations, taking what I've learned from what I've found online, I've stopped and cleaned things up to this point.

I have my list view displaying dogs and have my TextField I am using a stateful widget so I can use init for the TextController listener and dispose to dispose of it when done My text controller is setup with the controller and I'm initializing a variable that will hold the search text to controller.text I am fairly certain I will need to use the onChanged of the TextField to use the value with my search facility (in some way)

Currently, my listview builder is getting the list based on dogData.dogs.length, but that includes no logic to filter things down since it's just a get from my Dogs class.

I can easily build a method in my does class that would return a list using .toLowerCase and .contains which accepts some text to use to build a new list with the filtered items, but I've spun out here a bunch too as I fail in getting it tied back to my dogData via the Consumer.

This is such a common task in apps, mobile apps, web, etc., so I have to believe there is a clean/elegant/correct (more than other ways) method of accomplishing this task. Just getting lost in the details, I suppose.

any help would be greatly appreciated. Thank you, Bob


Solution 1: chunhunghan

You can copy paste run full code below
The idea is like Todo App has 3 different UnmodifiableListView

UnmodifiableListView<Task> get allTasks, 
UnmodifiableListView<Task> get incompleteTasks
UnmodifiableListView<Task> get completedTasks

Todo's example https://dev.to/shakib609/create-a-todos-app-with-flutter-and-provider-jdh

You can use the following code snippet to return UnmodifiableListView you need based on search string and in onChanged call provider changeSearchString
so other part like ListView do not have to change

  String _searchString = "";

  UnmodifiableListView<Dog> get dogs => _searchString.isEmpty
      ? UnmodifiableListView(_myDogs)
      : UnmodifiableListView(
          _myDogs.where((dog) => dog.breed.contains(_searchString)));

  void changeSearchString(String searchString) {
    _searchString = searchString;
    print(_searchString);
    notifyListeners();
  }

  ...

  onChanged: (value) {
            Provider.of<Dogs>(context, listen: false)
                .changeSearchString(value);
          },

working demo search dog by bread

enter image description here

full code

import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'dart:collection';
import 'package:provider/provider.dart';

class Dogs extends ChangeNotifier {
  final List<Dog> _myDogs = [
    Dog(name: 'Mackie', age: 8, breed: 'Rottweiler'),
    Dog(name: 'Riley', age: 8, breed: 'Rottweiler'),
    Dog(name: 'Tank', age: 7, breed: 'Mastiff'),
    Dog(name: 'Tanner', age: 7, breed: 'Mastiff'),
    Dog(name: 'Rocky', age: 10, breed: 'Rottweiler'),
    Dog(name: 'Angel', age: 11, breed: 'Poodle'),
    Dog(name: 'Roxie', age: 8, breed: 'St. Bernard'),
    Dog(name: 'Spud', age: 8, breed: 'St. Bernard'),
    Dog(name: 'Nick', age: 4, breed: 'Rottweiler'),
  ];

  String _searchString = "";

  UnmodifiableListView<Dog> get dogs => _searchString.isEmpty
      ? UnmodifiableListView(_myDogs)
      : UnmodifiableListView(
          _myDogs.where((dog) => dog.breed.contains(_searchString)));

  void changeSearchString(String searchString) {
    _searchString = searchString;
    print(_searchString);
    notifyListeners();
  }
}

class Dog {
  final String breed;
  final String name;
  final int age;

  Dog({this.breed, this.name, this.age});
}

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => Dogs(),
      child: MyApp(),
    ),
  );
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final _controller = TextEditingController();
  String _searchText;

  @override
  void initState() {
    _controller.addListener(
      () {
        setState(() {
          _searchText = _controller.text;
        });
      },
    );
    super.initState();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Dogs',
      home: Scaffold(
        appBar: AppBar(
          title: Text('Dogs'),
        ),
        body: Column(
          mainAxisAlignment: MainAxisAlignment.start,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            TextField(
              controller: _controller,
              decoration: InputDecoration(
                hintText: "Search",
                prefixIcon: Icon(Icons.search),
                border: OutlineInputBorder(
                  borderRadius: BorderRadius.all(
                    Radius.circular(4.0),
                  ),
                ),
              ),
              onChanged: (value) {
                Provider.of<Dogs>(context, listen: false)
                    .changeSearchString(value);
              },
            ),
            Consumer<Dogs>(builder: (context, dogData, child) {
              print(dogData.dogs.toString());
              return Expanded(
                child: ListView.builder(
                    shrinkWrap: true,
                    itemCount: dogData.dogs.length,
                    itemBuilder: (context, index) => Card(
                          elevation: 3,
                          child: ListTile(
                            title: Text(dogData.dogs[index].name),
                          ),
                        )),
              );
            }),
          ],
        ),
      ),
    );
  }
}