4
votes

In the efforts of learning Flutter framework / Dart, I created a sample project.

I have a class called person.dart with the following content:

class Person {
  String personFirstName;
  String personLastName;

  Person(
    {this.personFirstName, this.personLastName}
  );
}

Next, I have a "builder" class, person_builder.dart, where I create sample data of people:

import 'package:adv_search/model/person.dart';

class PersonDataBuilder {
  List getPeople() {
    return [
      Person(
          personFirstName: "John",
          personLastName: "Smith"
      ),
      Person(
          personFirstName: "Alex",
          personLastName: "Johnson"
      ),
      Person(
          personFirstName: "Jane",
          personLastName: "Doe"
      ),
      Person(
          personFirstName: "Eric",
          personLastName: "Johnson"
      ),
      Person(
          personFirstName: "Michael",
          personLastName: "Eastwood"
      ),
      Person(
          personFirstName: "Benjamin",
          personLastName: "Woods"
      ),
      Person(
          personFirstName: "Abraham",
          personLastName: "Atwood"
      ),
      Person(
          personFirstName: "Anna",
          personLastName: "Clack"
      ),
      Person(
          personFirstName: "Clark",
          personLastName: "Phonye"
      ),
      Person(
          personFirstName: "Kerry",
          personLastName: "Mirk"
      ),
      Person(
          personFirstName: "Eliza",
          personLastName: "Wu"
      ),
      Person(
          personFirstName: "Jackey",
          personLastName: "Lee"
      ),
      Person(
          personFirstName: "Kristin",
          personLastName: "Munson"
      ),
      Person(
          personFirstName: "Oliver",
          personLastName: "Watson"
      ),

    ];
  }
}

I have added search functionality to the top navigation bar just fine... upon clicking the search icon, the search field opens up (within the top nav) allowing me to provide search input. I have a controller that has a listener, capturing user input just fine, as shown in my main.dart file:

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:adv_search/model/person.dart';
import 'package:adv_search/data/person_builder.dart';

void main() => runApp(new AdvancedSearch());

class AdvancedSearch extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'List of People',
      home: new ListPersonPage(title: 'List of People'),
    );
  }
}

class ListPersonPage extends StatefulWidget {
  ListPersonPage({Key key, this.title}) : super(key: key);

  final String title;

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

class _ListPersonPageState extends State<ListPersonPage> {
  List people;
  TextEditingController controller = new TextEditingController();
  String filter;

  Widget appBarTitle = new Text("List of People");
  Icon actionIcon = new Icon(Icons.search);

  @override
  void initState() {
    PersonDataBuilder pdb = new PersonDataBuilder();
    people = pdb.getPeople();
    controller.addListener(() {
      setState(() {
        filter = controller.text;
      });
    });
    super.initState();
  }

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

  @override
  Widget build(BuildContext context) {
    final appTopAppBar = AppBar(
      elevation: 0.1,
      title: appBarTitle,
      actions: <Widget>[
        new IconButton(
          icon: actionIcon,
          onPressed: () {

            setState(() {
              if (this.actionIcon.icon == Icons.search) {
                this.actionIcon = new Icon(Icons.close);
                this.appBarTitle = new TextField(
                  style: new TextStyle(
                    color: Colors.white,
                  ),
                  decoration: new InputDecoration(
                      prefixIcon: new Icon(Icons.search, color: Colors.white),
                      hintText: "Search...",
                      hintStyle: new TextStyle(color: Colors.white)),
                  controller: controller,
                );
              } else {
                this.actionIcon = new Icon(Icons.search);
                this.appBarTitle = new Text("List of People");
              }
            });

          },
        ),
      ],
    );

    ListTile personListTile(Person person) => ListTile(
      title: Text(
        person.personFirstName + " " + person.personLastName,
        style: TextStyle(color: Colors.black45, fontWeight: FontWeight.bold),
      ),);

    Card personCard(Person person) => Card(
      child: Container(
        decoration: BoxDecoration(color: Colors.grey[300]),
        child: personListTile(person),
      ),
    );

    final appBody = Container(
      child: ListView.builder(
        scrollDirection: Axis.vertical,
        shrinkWrap: true,
        itemCount: people.length,
        itemBuilder: (BuildContext context, int index) {
          //return filter == null || filter == "" ? personCard(people[index]) : people[index].contains(filter) ? personCard(people[index]) : new Container();
          return filter == null || filter == "" ? personCard(people[index]) : new Container();
        },
      ),
    );

    return Scaffold(
      appBar: appTopAppBar,
      body: appBody,
    );
  }
}

However, where I am stuck at, and seeking guidance, is in the ListView.builder

This is what I am currently returning in ListView.builder (line 106)--

return filter == null || filter == "" ? personCard(people[index]) : people[index].contains(filter) ? personCard(people[index]) : new Container();

The error I am getting is:

NoSuchMethodError: Class 'Person' has no instance method 'contains'

Receiver: Instance of 'Person'

Tried calling: contains("John")

Currently, I cannot filter at all, given the error above. I would like to know:

  1. How can I allow a user to search by either first or last name and have the view refresh with the filtered cards of people found, based on user input?
  2. When I click on the search icon, the input field is not auto-focused... am I setting up my search functionality the correct way?
  3. Is there a better/recommended way to create the data (list of people)?

EDIT 1

I should add: Upon launch of the app, I can see a list of ALL people that were created through the builder class.

EDIT 2

Added ALL code files in their entirety; re-phrased parts of the post and added couple additional questions.

2

2 Answers

4
votes

The error is telling you precisely what is wrong with your code. When you write items[index].contains(searchFilter) , the compiler tries to find 'contains' method inside Person class. And since you have not implemented it, it is throwing an exception.

One way to implement search is as below :

List<Person> _personList = [] ;
List<Person> _searchList = [] ;

// populate _personList

_personList.forEach((p) {
   if (p.personFirstName == searchFilter or p.personLastName == searchFilter) {
       _searchList.add(f);  
   }
}

And then you show the _searchList instead of full _personList in the list view. For example, like below :

    Widget _buildPersonListView() {
      if (!_isSearching) {
         return _listContents(_personList);
      } else {
         return _listContents(_searchList);
      }

Then you define _listContents as below :

Widget _listContents(List<Person> list) {
    // implement list view
}

Have your widget build method as :

 @override
  Widget build(BuildContext context) {
    return Scaffold(
      .....
      appBar: buildBar(context),
      body: _buildPersonView()
    );
  }

Finally, set _isSearching based on the user interaction.

2
votes

So this was not trivial... most likely because I am new to Dart / Flutter framework... In any case, this answer builds on what @Sukhi answered:

First, the getPeople method in person_buider.dart needs to return of type List<Person>

Lastly, in the main.dart file, the following changes occurred:

  1. inside _ListPersonPageState State, we need to define two distinct lists (as @Sukhi mentioned): List<Person> _personList = [] and List<Person> _filteredList = []

  2. Inside the initState method:

    a. We need to create a temporary list, iterate over the list retrieved from the getPeople method of the PeopleDataBuilder class, and add each item in the returned list to this temporary list that we created

    b. Then, we setState and inside it, we assign this temporary list to the _personList and then assign that _personList to _filteredList

    c. When we add a listener to the controller, we do some (sanity) validation and based on that, we setState

  3. Inside the build method, we need to (yet again) create a temporary list (if the filter is not empty) and then assign that temporary list to _filteredList

  4. There were some quirks... such as cursor not appearing when the search icon was clicked, etc... so basic properties were added/defined for the TextField (e.g. cursorColor: Colors.white,, etc.)

NOTE: Creating these temporary lists is important... otherwise you will get error when using the search (I think the error was Concurrent modification during iteration, if I am not mistaken)

Full main.dart code, for reference:

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:adv_search/model/person.dart';
import 'package:adv_search/data/person_builder.dart';

void main() => runApp(new AdvancedSearch());

class AdvancedSearch extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'List of People',
      home: new ListPersonPage(title: 'List of People'),
    );
  }
}

class ListPersonPage extends StatefulWidget {
  ListPersonPage({Key key, this.title}) : super(key: key);

  final String title;

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

class _ListPersonPageState extends State<ListPersonPage> {
  List<Person> _personList = [];
  List<Person> _filteredList = [];
  TextEditingController controller = new TextEditingController();
  String filter = "";

  Widget appBarTitle = new Text("List of People");
  Icon actionIcon = new Icon(Icons.search);

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

  @override
  void initState() {
    PersonDataBuilder pdb = new PersonDataBuilder();
    List<Person> tmpList = new List<Person>();
    for(int i=0; i < pdb.getPeople().length; i++) {
      tmpList.add(pdb.getPeople()[i]);
    }
    setState(() {
      _personList = tmpList;
      _filteredList = _personList;
    });
    controller.addListener(() {
      if(controller.text.isEmpty) {
        setState(() {
          filter = "";
          _filteredList = _personList;
        });
      } else {
        setState(() {
          filter = controller.text;
        });
      }
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {

    final appTopAppBar = AppBar(
      elevation: 0.1,
      title: appBarTitle,
      actions: <Widget>[
        new IconButton(
          icon: actionIcon,
          onPressed: () {
            setState(() {
              if (this.actionIcon.icon == Icons.search) {
                this.actionIcon = new Icon(Icons.close);
                this.appBarTitle = new TextField(
                  controller: controller,
                  decoration: new InputDecoration(
                    prefixIcon: new Icon(Icons.search, color: Colors.white),
                    hintText: "Search...",
                    hintStyle: new TextStyle(color: Colors.white),
                  ),
                  style: new TextStyle(
                    color: Colors.white,
                  ),
                  autofocus: true,
                  cursorColor: Colors.white,
                );
              } else {
                this.actionIcon = new Icon(Icons.search);
                this.appBarTitle = new Text("List of People");
                _filteredList = _personList;
                controller.clear();
              }
            });
          },
        ),
      ],
    );

    ListTile personListTile(Person person) => ListTile(
      title: Text(
        person.personFirstName + " " + person.personLastName,
        style: TextStyle(color: Colors.black45, fontWeight: FontWeight.bold),
      ),);

    Card personCard(Person person) => Card(
      child: Container(
        decoration: BoxDecoration(color: Colors.grey[300]),
        child: personListTile(person),
      ),
    );

    if((filter.isNotEmpty)) {
      List<Person> tmpList = new List<Person>();
      for(int i = 0; i < _filteredList.length; i++) {
        if(_filteredList[i].personFirstName.toLowerCase().contains(filter.toLowerCase())) {
          tmpList.add(_filteredList[i]);
        }
      }
      _filteredList = tmpList;
    }

    final appBody = Container(
      child: ListView.builder(
        scrollDirection: Axis.vertical,
        shrinkWrap: true,
        itemCount: _personList == null ? 0 : _filteredList.length,
        itemBuilder: (BuildContext context, int index) {
          return personCard(_filteredList[index]);
        },
      ),
    );

    return Scaffold(
      appBar: appTopAppBar,
      body: appBody,
    );
  }
}