Flutter - Implementing a listView search feature

7,388

Solution 1

There are probably many ways to implement this based on the resulting experience you want. A simple solution is to create activeSearch state that toggles a 'search app bar' and a 'normal app bar'

Here's the normal app bar:

return AppBar(
  title: Text("My App"),
  actions: <Widget>[
    IconButton(
      icon: Icon(Icons.search),
      onPressed: () => setState(() => activeSearch = true),
    ),
  ],
);

And here's the search app bar:

return AppBar(
  leading: Icon(Icons.search),
  title: TextField(
    decoration: InputDecoration(
      hintText: "here's a hint",
    ),
  ),
  actions: <Widget>[
    IconButton(
      icon: Icon(Icons.close),
      onPressed: () => setState(() => activeSearch = false),
    )
  ],
);

Note: if you don't want to have a leading icon when search is active you may want to disable the default behavior for a drawer and back button icon with:

automaticallyImplyLeading: false

Full example:

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

class _MyAppState extends State<MyApp> {
  bool activeSearch;

  @override
  void initState() {
    super.initState();
    activeSearch = false;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: _appBar(),
      drawer: _drawer(),
    );
  }

  PreferredSizeWidget _appBar() {
    if (activeSearch) {
      return AppBar(
        leading: Icon(Icons.search),
        title: TextField(
          decoration: InputDecoration(
            hintText: "here's a hint",
          ),
        ),
        actions: <Widget>[
          IconButton(
            icon: Icon(Icons.close),
            onPressed: () => setState(() => activeSearch = false),
          )
        ],
      );
    } else {
      return AppBar(
        title: Text("My App"),
        actions: <Widget>[
          IconButton(
            icon: Icon(Icons.search),
            onPressed: () => setState(() => activeSearch = true),
          ),
        ],
      );
    }
  }

  Widget _drawer() {
    return Container();
  }
}

enter image description here

enter image description here

UPDATE: Here's a hint at handling results

return AppBar(
  ...
  title: TextField(
    onChanged: _search,
  ),
);

And what _search could look like:

  List<MyResultObject> _results;

  void _search(String queryString) {
    // do some searching and sorting
    // then call setState() with the results
    // and then in your ListView you can read from results
    // (handle empty, default case as well in view)
    setState(() {
      _results = ...
    });
  }

  List<Widget> _resultWidgets() {
    if (_results.isEmpty) return _defaultWidgets();
    _results.map((r) => _buildRowWidget(s)).toList();
  }

Solution 2

Can u refer a simple search view in this answer. In that example, as the user types, the list will get filtered.

Share:
7,388
Jake
Author by

Jake

Work email: jake(at)squaredsoftware.co.uk Please support me by downloading my new iOS application, travelrecce!

Updated on December 06, 2022

Comments

  • Jake
    Jake over 1 year

    I've been trying to implement a search bar into my app for bringing selected listView items to the top of a list. The list contains quite a few items, around approximately 1700 so the addition of a search bar is essential. I'd like the listView search box to appear from a search icon on the right hand side of the top appBar. Below is a picture of the current view for reference.

    When you click the search iconButton a search field should replace the title in the appBar. It's going to be evident to the user that this is for the crypto listView as I'll add a hint in the search view identifying this.

    current crypto app view

    I'm not including all my code as this would be cumbersome for a stack question, but below is my home_page.dart file, where as the rest of my classes for the bottom crypto listView can be found at this GitHub repo.

    This is what my 'home_page.dart` looks like;

    import 'package:cryptick/cryptoData/crypto_data.dart';
    import 'package:cryptick/cryptoData/trending_data.dart';
    import 'package:cryptick/modules/crypto_presenter.dart';
    import 'package:flutter/cupertino.dart';
    import 'package:flutter/gestures.dart';
    import 'package:flutter/material.dart';
    import 'background.dart';
    
    //FOLLOWING DART CODE COPYRIGHT OF 2017 - 2018 SQUARED SOFTWARE LONDON
    
    class HomePage extends StatefulWidget {
      @override
      _HomePageState createState() => new _HomePageState();
    }
    
    class ServerStatusScreen extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return new Scaffold(
          appBar: new AppBar(
            iconTheme: new IconThemeData(color: Colors.white),
            centerTitle: true,
            backgroundColor: Colors.black,
            title: new Text(
              'API Server Status',
              textAlign: TextAlign.center,
              style: new TextStyle(
                  color: Colors.white, fontSize: 27.5, fontFamily: 'Kanit'),
            ),
          ),
          body: new Center(
            child: new Column(
              children: [
                new Divider(color: Colors.white),
                new Text(
                  'News Feed: ',
                  textAlign: TextAlign.center,
                  style: new TextStyle(
                    color: Colors.black,
                    fontSize: 27.5,
                    fontFamily: 'Kanit',
                  ),
                ),
                new Divider(),
                new Text(
                  'Crypto Feed: ',
                  textAlign: TextAlign.center,
                  style: new TextStyle(
                    color: Colors.black,
                    fontSize: 27.5,
                    fontFamily: 'Kanit',
                  ),
                ),
                new Divider(),
                new Wrap(
                  alignment: WrapAlignment.center,
                  children: <Widget>[
                    new Chip(
                      backgroundColor: Colors.black,
                      label: new Text(
                        '© 2017-2018 Squared Software',
                        style: new TextStyle(
                          fontSize: 15.0,
                          fontFamily: 'Poppins',
                          color: Colors.white,
                        ),
                      ),
                    ),
                  ],
                ),
              ],
            ),
          ),
        );
      }
    }
    
    class MoreInfoScreen extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        final ThemeData themeData = Theme.of(context);
        final TextStyle aboutTextStyle = themeData.textTheme.body2;
        final TextStyle linkStyle =
            themeData.textTheme.body2.copyWith(color: themeData.accentColor);
        return new Scaffold(
          appBar: new AppBar(
            iconTheme: new IconThemeData(color: Colors.white),
            centerTitle: true,
            backgroundColor: Colors.black,
            title: new Text(
              'More Info',
              textAlign: TextAlign.center,
              style: new TextStyle(
                  color: Colors.white, fontSize: 27.5, fontFamily: 'Kanit'),
            ),
          ),
          body: new Center(
            child: new Column(
              children: [
                new Divider(color: Colors.white),
                new ListTile(
                    title: new Text('Squared Software',
                        style: new TextStyle(
                          fontWeight: FontWeight.w500,
                          fontFamily: 'Poppins',
                        )
                      ),
                    leading: new CircleAvatar(
                        radius: 30.0,
                        backgroundImage: new AssetImage(
                            'images/sqinterlock.png'
                            )
                          )
                        ),
                new Divider(),
                new Text('Where do we get our information?',
                    style: new TextStyle(
                      color: Colors.black,
                      fontFamily: 'Poppins',
                      fontSize: 16.5,
                    )
                  ),
                new Divider(color: Colors.white),
                new Text(
                  "News Feed: bit.ly/2MFpzHX",
                  style: new TextStyle(
                    fontFamily: 'Poppins',
                    fontSize: 16.5,
                  ),
                ),
                new Divider(color: Colors.white),
                new Text(
                  "Crypto Feed: bit.ly/2iIdJht",
                  style: new TextStyle(
                    fontFamily: 'Poppins',
                    fontSize: 16.5,
                  ),
                ),
                new Divider(color: Colors.white),
                new Wrap(
                  alignment: WrapAlignment.center,
                  children: <Widget>[
                    new Chip(
                      backgroundColor: Colors.black,
                      label: new Text(
                        '© 2017-2018 Squared Software',
                        style: new TextStyle(
                          fontSize: 15.0,
                          fontFamily: 'Poppins',
                          color: Colors.white,
                        ),
                      ),
                    ),
                  ],
                ),
              ],
            ),
          ),
        );
      }
    }
    
    class _HomePageState extends State<HomePage> implements CryptoListViewContract {
      CryptoListPresenter _presenter;
      List<Crypto> _currencies;
      bool _isLoading;
      final List<MaterialColor> _colors = [Colors.blue, Colors.indigo, Colors.red];
    
      _HomePageState() {
        _presenter = new CryptoListPresenter(this);
      }
    
      @override
      void onLoadTrendingComplete(Trending trending) {
        // TODO:
        articlesMap = trending.articles;
    
        for (Map articleMap in articlesMap) {
          articles.add(Articles.fromMap(articleMap));
        }
    
        if (mounted) setState(() {});
      }
    
      @override
      void onLoadTrendingError() {
        // TODO:
      }
    
      List articlesMap = [];
      List<Articles> articles = [];
    
      @override
      void initState() {
        super.initState();
        _isLoading = true;
        _presenter.loadCurrencies();
        _presenter.loadTrending();
      }
    
      @override
      Widget build(BuildContext context) {
        return new Scaffold(
            appBar: new AppBar(
              title: new Text(
                "Cryp - Tick Exchange",
                style: new TextStyle(
                  color: Colors.white,
                  fontFamily: 'Poppins',
                  fontSize: 22.5,
                ),
              ),
              iconTheme: new IconThemeData(color: Colors.white),
              backgroundColor: const Color(0xFF273A48),
              elevation: 0.0,
              centerTitle: true,
            ),
            drawer: new Drawer(
              child: new ListView(padding: EdgeInsets.zero, children: <Widget>[
                new DrawerHeader(
                  child: new CircleAvatar(
                    child: new Image.asset('images/ctavatar.png'),
                  ),
                  decoration: new BoxDecoration(
                    color: Colors.black,
                  ),
                ),
                new MaterialButton(
                    child: new Text(
                      'Server Status',
                      textAlign: TextAlign.center,
                      style: new TextStyle(fontSize: 27.5, fontFamily: 'Kanit'),
                    ),
                    onPressed: () {
                      Navigator.push(
                        context,
                        MaterialPageRoute(
                            builder: (context) => ServerStatusScreen()),
                      );
                    }),
                new Divider(),
                new MaterialButton(
                    child: new Text(
                      'More Info',
                      textAlign: TextAlign.center,
                      style: new TextStyle(fontSize: 27.5, fontFamily: 'Kanit'),
                    ),
                    onPressed: () {
                      Navigator.push(
                        context,
                        MaterialPageRoute(builder: (context) => MoreInfoScreen()),
                      );
                    }),
                new Divider(),
                new Wrap(
                  alignment: WrapAlignment.center,
                  children: <Widget>[
                    new Chip(
                      backgroundColor: Colors.black,
                      label: new Text(
                        'v0.0.1',
                        style: new TextStyle(
                          fontSize: 15.0,
                          fontFamily: 'Poppins',
                          color: Colors.white,
                        ),
                      ),
                    ),
                  ],
                ),
              ]),
            ),
            body: _isLoading
                ? new Center(child: new CupertinoActivityIndicator(radius: 15.0))
                : _allWidget());
      }
    
    
      Widget _allWidget() {
        final _width = MediaQuery.of(context).size.width;
        final _height = MediaQuery.of(context).size.height;
    //CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED
        final headerList = new ListView.builder(
          itemBuilder: (context, index) {
            EdgeInsets padding = index == 0
                ? const EdgeInsets.only(
                    left: 20.0, right: 10.0, top: 4.0, bottom: 30.0)
                : const EdgeInsets.only(
                    left: 10.0, right: 10.0, top: 4.0, bottom: 30.0);
            return new Padding(
              padding: padding,
              child: new InkWell(
                onTap: () {
                  print('@url');
                },
                child: new Container(
                  decoration: new BoxDecoration(
                    borderRadius: new BorderRadius.circular(10.0),
                    color: const Color(0xFF273A48),
                    boxShadow: [
                      new BoxShadow(
                        color: Colors.black.withAlpha(70),
                        offset: const Offset(3.0, 10.0),
                          blurRadius: 15.0)
                    ],
                    image: new DecorationImage(
                      image: new NetworkImage(articles[index].urlToImage),
                      fit: BoxFit.fitHeight,
                    ),
                  ),
                  height: 200.0,
                  width: 275.0,
                  child: new Stack(
                    children: <Widget>[
                      new Align(
                        alignment: Alignment.bottomCenter,
                        child: new Container(
                          padding: new EdgeInsets.only(left: 10.0),
                            decoration: new BoxDecoration(
                              color: const Color(0xFF273A48),
                              borderRadius: new BorderRadius.only(
                                  bottomLeft: new Radius.circular(10.0),
                                  bottomRight: new Radius.circular(10.0)),
                            ),
                            height: 50.0,
                            child: new Row(
                              mainAxisAlignment: MainAxisAlignment.center,
                              children: <Widget>[
                                new Expanded(child: new Text(
                                  articles[index].title,
                                  overflow: TextOverflow.ellipsis,
                                  maxLines: 2,
                                  style: new TextStyle(
                                    color: Colors.white,
                                    fontFamily: 'Poppins',
                                  ),
                                ),
                              ),
                            ],
                          )
                        ),
                      )
                    ],
                  ),
                ),
              ),
            );
          },
          scrollDirection: Axis.horizontal,
          itemCount: articles.length,
        );
    
        final body = new Scaffold(
          backgroundColor: Colors.transparent,
          body: new Container(
            child: new Stack(
              children: <Widget>[
                new Padding(
                  padding: new EdgeInsets.only(top: 10.0),
                  child: new Column(
                    crossAxisAlignment: CrossAxisAlignment.center,
                    mainAxisSize: MainAxisSize.max,
                    mainAxisAlignment: MainAxisAlignment.start,
                    children: <Widget>[
                      new Align(
                        alignment: Alignment.centerLeft,
                        child: new Padding(
                            padding: new EdgeInsets.only(
                              left: 10.0,
                            ),
                            child: new Text(
                              "Trending News",
                              style: new TextStyle(
                                letterSpacing: 0.8,
                                fontFamily: 'Kanit',
                                fontSize: 17.5,
                                color: Colors.white,
                              ),
                            )),
                      ),
                      new Container(
                          height: 300.0, width: _width, child: headerList),
                      new Expanded(child: ListView.builder(
                          itemBuilder: (BuildContext context, int index) {
                        final int i = index;
                        final Crypto currency = _currencies[i];
                        final MaterialColor color = _colors[i % _colors.length];
                        return new ListTile(
                          title: new Column(
                            children: <Widget>[
                              new Row(
                                crossAxisAlignment: CrossAxisAlignment.start,
                                children: <Widget>[
                                  new Container(
                                    height: 72.0,
                                    width: 72.0,
                                    decoration: new BoxDecoration(
                                        color: Colors.white,
                                        boxShadow: [
                                          new BoxShadow(
                                              color: Colors.black.withAlpha(80),
                                              offset: const Offset(2.0, 2.0),
                                              blurRadius: 15.0)
                                        ],
                                        borderRadius: new BorderRadius.all(
                                            new Radius.circular(35.0)),
                                        image: new DecorationImage(
                                          image: new ExactAssetImage(
                                            "cryptoiconsBlack/" +
                                                currency.symbol.toLowerCase() +
                                                "@2x.png",
                                          ),
                                          fit: BoxFit.cover,
                                        )),
                                  ),
                                  new SizedBox(
                                    width: 8.0,
                                  ),
                                  new Expanded(
                                      child: new Column(
                                    mainAxisAlignment: MainAxisAlignment.start,
                                    crossAxisAlignment: CrossAxisAlignment.start,
                                    children: <Widget>[
                                      new Text(
                                        currency.name,
                                        style: new TextStyle(
                                            fontSize: 15.0,
                                            fontFamily: 'Poppins',
                                            color: Colors.black87,
                                            fontWeight: FontWeight.bold),
                                      ),
                                      _getSubtitleText(currency.price_usd,
                                          currency.percent_change_1h),
                                    ],
                                  )),
                                ],
                              ),
                              new Divider(),
                            ],
                          ),
                        );
                      }))
                    ],
                  ),
                ),
              ],
            ),
          ),
        );
    
        return new Container(
          decoration: new BoxDecoration(
            color: const Color(0xFF273A48),
          ),
          child: new Stack(
            children: <Widget>[
              new CustomPaint(
                size: new Size(_width, _height),
                painter: new Background(),
              ),
              body,
            ],
          ),
        );
      }
    
    // CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED CRYPTO FEED
    
      Widget _getSubtitleText(String priceUSD, String percentageChange) {
        TextSpan priceTextWidget = new TextSpan(
            text: "\$$priceUSD\n",
            style: new TextStyle(
              color: Colors.black,
              fontSize: 14.0,
            ));
        String percentageChangeText = "1 hour: $percentageChange%";
        TextSpan percentageChangeTextWidget;
    
        if (double.parse(percentageChange) > 0) {
          percentageChangeTextWidget = new TextSpan(
              text: percentageChangeText,
              style: new TextStyle(
                color: Colors.green,
                fontFamily: 'PoppinsMediumItalic',
              ));
        } else {
          percentageChangeTextWidget = new TextSpan(
              text: percentageChangeText,
              style: new TextStyle(
                color: Colors.red,
                fontFamily: 'PoppinsMediumItalic',
              ));
        }
    
        return new RichText(
            text: new TextSpan(
                children: [priceTextWidget, percentageChangeTextWidget]));
      }
    
      //Works with cryptoListViewContract implimentation in _MyHomePageState
      @override
      void onLoadCryptoComplete(List<Crypto> items) {
        // TODO: implement onLoadCryptoComplete
    
        setState(() {
          _currencies = items;
          _isLoading = false;
        });
      }
    
      @override
      void onLoadCryptoError() {
        // TODO: implement onLoadCryptoError
      }
    }
    

    Thanks for the help, Jake

    • Ashton Thomas
      Ashton Thomas over 5 years
      Have you consider AppBar.actions? If so, what is the desired affect? That is, do you want a have a dialog pop up to search? Would be cool to have an TextField instead of an Icon and you just expand that our while hiding the other parts of the AppBar.
    • Jake
      Jake over 5 years
      Yeah I'll edit the question after this to reflect this comment but is there anyway when you click the search iconButton a search field could replace the title in the appBar?. It's going to be evident to the user that this is for the crypto listView as I'll add a hint in the search view identifying this. Thanks
    • Paresh Mangukiya
      Paresh Mangukiya almost 4 years
      To use this plugin And use it however you want :- stackoverflow.com/a/61727414/10563627
    • Paresh Mangukiya
      Paresh Mangukiya almost 4 years
      There's the Flutter library you could use to implement the same sort of design that Youtube/Instagram uses : github.com/pkmangukiya/flutter_search_view_pk
  • Jake
    Jake over 5 years
    This looks great, just implementing it now
  • Jake
    Jake over 5 years
    I'll implement the question from Ashton first, then I'll add that after. Thanks for the additional info :)
  • Jake
    Jake over 5 years
    Is there anyway you could provide or hint at the code I would need to actually sort the items? The search bar is really good thanks for what you've done so far.
  • Ashton Thomas
    Ashton Thomas over 5 years
    @Jake, I added quick update at the end that may help
  • Jake
    Jake over 5 years
    Brilliant, thanks for the help. I'll take a look at the question @Dinesh Balasubramanian provided and reference it soon.
  • Jake
    Jake over 5 years
    Hey @Ashton, just noticed that when the on screen iOS keyboard pops up when I type a value to the search field, I get a severe pixel overflow. Anything I could do about this? Thanks
  • Ashton Thomas
    Ashton Thomas over 5 years
    You need to wrap the body of your scaffold in a listview to creat a scrollable area