Reputation: 1206
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
Upvotes: 2
Views: 8952
Reputation: 54377
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
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),
),
)),
);
}),
],
),
),
);
}
}
Upvotes: 10