So first of all let me show you an example of how to solve this by using your approach - please read the comments inside the code blocks as well - those should help to better understand what I did there!
Spoiler: We don't want to do it like this in Flutter because it will lead to pass down objects / functions everywhere and it will get very hard to maintain such code - there are good solutions for this "problem" which is summarised under "State Management".
I created a class which is used to hold the current search query entered in the search bar:
class SearchModel {
String searchString = '';
}
Then we have our minimalistic view where we use the Scaffold widget and (just as in your example) an AppBar and a ListView:
class HomeView extends StatefulWidget {
@override
_HomeViewState createState() => _HomeViewState();
}
class _HomeViewState extends State<HomeView> {
SearchModel _searchModel = SearchModel();
_updateSearch(String searchQuery) {
/// The setState function is provided by StatefulWidget, every Widget we
/// create which extends StatefulWidget has access to this function. By calling
/// this function we essentially say Flutter we want the Widget (which is coupled
/// with this state of this StatefulWidget) that we want to rebuild (call the build
/// function again)
setState(() {
_searchModel.searchString = searchQuery;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: PreferredSize(
/// We pass down the _updateSearch function to our AppBar so when the user
/// changes the text in the TextField it will update the searchString in our
/// SearchModel object and call setState so we rebuild HomeViewState (which is
/// currently the root of our app, so everything gets rebuilded)
child: MyAppBar(
searchFunction: _updateSearch,
),
preferredSize: Size.fromHeight(kToolbarHeight)),
/// In MyListView, where we use the ListView internally to show the results, we
/// just pass down our SearchModel object where the searchString is maintained
/// so we can filter our list
body: MyListView(
searchModel: _searchModel,
),
);
}
}
Now our AppBar where the user can input something and search our list:
class MyAppBar extends StatefulWidget {
/// This is how we declare a function type in Dart where we say which
/// kind of parameters (here String) it will use
final Function(String) searchFunction;
MyAppBar({this.searchFunction});
@override
_MyAppBarState createState() => _MyAppBarState();
}
class _MyAppBarState extends State<MyAppBar> {
@override
Widget build(BuildContext context) {
return AppBar(
title: TextField(
/// onChanged of TextField needs a function where we pass down
/// a String and do what we want, thats what the searchFunction is for!
onChanged: widget.searchFunction,
),
);
}
}
Last but not least the ListView Widget where we display some elements and if the user changes the input in the TextField of our AppBar, we want to filter those elements and only show those which match with the searchQuery:
class MyListView extends StatefulWidget {
final SearchModel searchModel;
MyListView({this.searchModel});
@override
_MyListViewState createState() => _MyListViewState();
}
class _MyListViewState extends State<MyListView> {
List<String> games = [
'Anno 1602',
'Final Fantasy 7',
'Final Fantasy 8',
'Dark Souls'
];
@override
Widget build(BuildContext context) {
return ListView(
/// Some minimalistic usage of functions which are usable on List objects:
/// I initialised a random List of game names as Strings and the first thing
/// I want to do is to filter out all the games which contain the same String
/// pattern like the searchQuery which the user entered - this is done with the
/// where function. The where function returns the filtered list, on this filtered
/// list i use the map function, which takes every object from this list and "maps"
/// it to whatever we want, here for every game String I want a ListTile Widget which
/// is being used for our ListView! At the end I have to call toList() since those
/// functions return so called Iterables instead of List object, "basically" the same
/// but different classes so we just change the Iterables to List
children: games
.where((game) => game
.toLowerCase()
.contains(widget.searchModel.searchString.toLowerCase()))
.map(
(game) => ListTile(
title: Text(game),
),
)
.toList(),
);
}
}
Well so this works, BUT as I said at the beginning, doing it like this will force you to pass down objects / functions to every Widget literally everywhere! Once your app gets big enough all your Widgets will have dozens of parameters and you will very fast forget where you did what and maintaining, improving, expanding your code gets nearly impossible.
Thats why we need something called State Management! Even though Flutter is quite new, at least compared to other well known frameworks, the community came up with a lot of different solutions / approaches for State Management. You should read about it yourself to find out whats the best solution for you - a lot comes down to personal preference actually. Since I personally love and use Provider (which you can use just by its own as the solution) together with MobX, I will show you how you could deal with this example by just using Provider (https://pub.dev/packages/provider):
First our SearchModel, which now got extended:
/// We extend our classes which are basically our "States" with ChangeNotifier
/// so we enable our class to notify all listeners about a change - you will see
/// why!
class SearchModel extends ChangeNotifier {
String searchString = '';
/// Now we don't update the searchString variable directly anymore, we use a
/// function because we need to call notifiyListeners every time we change something
/// where we want to notify everyone and all the listeners can react to this change!
updateSearchString(searchQuery) {
this.searchString = searchQuery;
notifyListeners();
}
}
Now our new AppBar:
/// Our own Widgets MyAppBar and MyListView are not Stateless! We don't need the state
/// anymore (at least in this example) since we won't use setState anymore and tell
/// our Widgets to rebuild, but will make use of a Widget provided by the Provider
/// package which will rebuild itself dynamically! More later in MyListView
class MyAppBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AppBar(
title: TextField(
/// context.watch is also a function added through the Provider package where
/// we can access objects which have been provided by a Provider Widget (you
/// will see this in the HomeView implementation) and thats how we now pass
/// the function instead of passing it down to this Widget manually! Easier right?
onChanged: context.watch<SearchModel>().updateSearchString,
),
);
}
}
The updated ListView:
class MyListView extends StatelessWidget {
final List<String> games = [
'Anno 1602',
'Final Fantasy 7',
'Final Fantasy 8',
'Dark Souls'
];
@override
Widget build(BuildContext context) {
return ListView(
/// Just like in the MyAppBar implementation we can access our SearchModel object
/// directly by using context.watch instead of passing it down to our Widget!
/// again: very nice
/// The function itself hasn't been changed!
/// Since we are using the watch function on a object which extends ChangeNotifier,
/// every time it gets updated, this will get rebuilded automatically! Magic
children: games
.where((game) => game.toLowerCase().contains(
context.watch<SearchModel>().searchString.toLowerCase()))
.map(
(game) => ListTile(
title: Text(game),
),
)
.toList(),
);
}
}
And now our HomeView where we basically start this view:
/// Every Widget we created manually now is stateless since we don't manage any
/// state by ourself now, which reduces the boilerplate and makes accessing stuff easier!
/// Whats left: in our MyListView and MyAppBar Widgets we accessed the SearchModel
/// object with context.watch ... but how is this possible? Well, we need to provide
/// it somehow of course - thats where the Provider packages gets handy again!
class HomeView extends StatelessWidget {
@override
Widget build(BuildContext context) {
/// The Provider package has several Provider Widgets, one is the ChangeNotifierProvider
/// which can be used to provide objects which extend ChangeNotifier, just what we need!
///
/// Some background: we have to remember that Flutter is basically a big tree. Usually we
/// use a MaterialApp Widget as the root Widget and after that everything else is being
/// passed down as a child which will result in a big tree. What we do here: As early as we
/// need it / want to we place our Provider Widget and "pass down" our Model which should be
/// accessible for every Widget down the tree. Every Widget which is now under this Provider
/// (in our case MyAppBar and MyListView) can access this object with context.watch and
/// work with it
return ChangeNotifierProvider(
create: (_) => SearchModel(),
builder: (context, _) => Scaffold(
appBar: PreferredSize(
child: MyAppBar(), preferredSize: Size.fromHeight(kToolbarHeight)),
body: MyListView(),
),
);
}
}
A lot to read - sorry to make it this long but I wanted to make sure to show you the problem, how it could be solved trying your approach and why it is so important to learn and use State Management in Flutter as early as possible! I hope this gave you a good insight why its important and good and that it makes Flutter even more awesome.
The way I used Provider in this example is also just one of many ways to make use of this State Management solution - I highly recommend you to read about it yourself on the pub.dev site of Provider itself I linked earlier. There are many examples how and when you use different Widgets / approaches of Provider!
Hope this helps you out! :)