Flutter - Calling a function (with parameters) from parent to child

5,337

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! :)

Share:
5,337
Anthony D
Author by

Anthony D

Anthony

Updated on December 22, 2022

Comments

  • Anthony D
    Anthony D over 1 year

    I'm having trouble calling a method from a parent to a child, the setup is that I have my scaffold with 2 childs,a listView and an appbar. In the appbar i have a searchBar and i want to update the listView when i search on the searchBar.

    I have seen the videos from the flutter team on the streams but i think it's overkill for a simple case like this. I also found this thread "Flutter calling child class function from parent class" and the @ehsaneha response and i think it's quite good for my use case. I can get the data from the searchbar to the parent using a Function in the appBar constructor and then call this from the parent to the listView ! I did it and it works fine when you don't give any data to the function, but as soon as you need parameters ( like the string entered in searchbar in this case ) it doesn't work anymore. I have an error that says :

    a value of type 'dynamic Function(dynamic)' can't be assigned to a variable of type 'dynamic Function()'

    on the " = updateList " part :

      _ListViewState(ListViewController _controller) {
        _controller.updateList = updateList;
      }
    

    I'm a bit lost on this one, i'm not sure if i'm going on the right direction or not, there's no easy answer that i found on my searchs. I come from Angular and i couldn't find something as easy as an observable that i could just give to my listView so that it subscribe to it.

    Thanks in advance for any help !

    Edit :

    Following the comment i'm adding more code so that we can see my use case in more detail :

    So i have a homepage.dart that looks like this

    final ListViewController listViewController = ListViewController();
    
    Widget build(BuildContext context) {
     return Scaffold(
       appBar: MyAppBar(setSearchString: setSearchString),
       body: MyListView(controller: listViewController)
     )
    }
    ...
    
      setSearchString(String searchString) {
        listViewController.updateList();
      }
    

    In my listView i have setup everything i needed to be able to get to the updateList() function and it looks like this :

    class MyListView extends StatefulWidget {
      final ListViewController controller;
    
      MyListView({this.controller});
      @override
      _MyListViewState createState() => _MyListViewState(controller);
    }
    
    class _MyListViewState extends State<MyListView> {
      _MyListViewState(ListViewController _controller) {
        _controller.updateList = updateList;
      }
    
    ...
      updateList() {
        print('In updateList !');
      }
    }
    

    The searchbar part is just a case of using the fucntion passed in the constructor to get the searchText to the parent (homepage.dart), so i don't think it's needed

    So this is where i'm at, it's working fine for now but as soon as i want to add a parameter to updateList() like updateList(String text), it gives me the error i mentioned above. I hope i added enought code, tell me if it's otherwise and i'll try to add what is needed !

    • kounex
      kounex almost 4 years
      Would help a lot if you could provide more code for this use case - your widget layout regarding Searchbar and the ListView where you use it
    • Anthony D
      Anthony D almost 4 years
      @kounex Thanks for your answer I added more explanation and code, i hope it helps !
    • kounex
      kounex almost 4 years
      Alright I see what you mean - let me prepare an answer, hold tight!
  • Anthony D
    Anthony D almost 4 years
    Thanks a lot, sorry i was sick the last days but i'm actively reading about all the state management solution there is for flutter and i'm slowly getting the hang of it but it's hard to get everything. There's actualy so many different solution that i fint it hard to know which one to use but thanks a lot for pointing me in the right direction and for this detailed response !
  • kounex
    kounex over 3 years
    No problem! Glad I could help - if you need some additional advice or have any questions, feel free to hit me up! :)
  • Anthony D
    Anthony D over 3 years
    Sorry but i still don't get it, when i do the context.watch at the start of the build method it works fine but when it's on the "onFieldSubmitted" from a textFormField it doesn't, it seems like i don't get to the method. It even says that i'm using it outside of the build method which is wrong. I think the onpressed method and setState one doesn't allow me to get the value external to them, like the buildContext, so i don't have the right reference i guess? I tried the normal () {} way and also the () => {} but i can't get to the methods that i need still. Could you help me on this please ?
  • Anthony D
    Anthony D over 3 years
    Ok i get it when it's from a button you can't "listen" to the notifier, that's what was wrong on my part, it now works properly Thanks again @kounex !
  • kounex
    kounex over 3 years
    Yes correct - you can’t use watch inside callbacks like onPressed or something. In this case you can make use of the read function: context.read (but you probably figured it out yourself already) - glad it works fine for you now!