Parent to child communication in Flutter
Published on September 5, 22'
flutter

Table of contents

Introduction

Many developers are introduced to the concept of sharing data between nested components very early on and it is a fundamental concept needed to build complex user interfaces.

A situation that sometimes arises during development is how to call functions inside child components externally.

For instance: submitting a form from a parent component, setting the contents of a shopping cart to an empty state etc.

Controller

A common way of handling child function invocation, controllers are used frequently and are even referenced in the Flutter docs.

An object with several function properties is passed down to the child which in return overrides the default behaviour of those passed functions. Calling those functions from a parent component will directly impact the child.

class Controller {
  void Function()? doSomething;
}

class Parent extends StatelessWidget {
  final Controller _controller = Controller();

  Parent({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Child(controller: _controller),
        ElevatedButton(
          onPressed: () {
            _controller.doSomething!();
          },
          child: Text("Press me!"),
        )
      ],
    );
  }
}

class Child extends StatefulWidget {
  final Controller controller;

  Child({
    Key? key,
    required this.controller,
  }) : super(key: key);

  @override
  State<Child> createState() => _ChildState(controller);
}

class _ChildState extends State<Child> {
  void doSomething() {
    print("Hello!");
  }

  _ChildState(Controller _controller) {
    _controller.doSomething = doSomething;
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

GlobalKey

The concept of keys is shared across a multitude of UI frameworks, in essence they are unique identifiers used internally by the application to update the existing user interface.

By checking the component/widget tree and comparing it with a new one it can be updated more easily by isolating only the things that require a change.

The keys can also be used to directly manipulate a widget from anywhere in the application (making it similar to another common trap with new developers, being the notorious global variable), but it also opens up a multitude of problems if used uncaringly. However, in a simple case such as this it poses almost no threat.

class Parent extends StatelessWidget {
  final GlobalKey<_ChildState> _childKey = GlobalKey();

  Parent({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Child(key: _childKey),
        ElevatedButton(
          onPressed: () {
            _childKey.currentState!.doSomething();
          },
          child: Text("Press me!"),
        )
      ],
    );
  }
}

class Child extends StatefulWidget {
  Child({Key? key}) : super(key: key);

  @override
  State<Child> createState() => _ChildState();
}

class _ChildState extends State<Child> {
  void doSomething() {
    print("Hello!");
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

State management packages

A method with some boilerplate and additional setup, many state management packages and even Flutter itself offer a way of notifying deeply nested components of some event.

In this instance we'll be using RiverPod.

class Notifier extends StateNotifier<bool> {
  Notifier() : super(false);

  void toggle() {
    state = !state;
  }
}

final provider = StateNotifierProvider<Notifier, bool>((ref) {
  return Notifier();
});

class Parent extends ConsumerWidget {
  const Parent({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return Column(
      children: [
        Child(),
        ElevatedButton(
          onPressed: () {
            ref.read(provider.notifier).toggle();
          },
          child: Text("Press me!"),
        )
      ],
    );
  }
}

class Child extends ConsumerWidget {
  const Child({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    void doSomething() {
      print("Hello!");
    }

    ref.listen<bool>(provider, (bool? oldVal, bool newVal) {
      doSomething();
    });

    return Container();
  }
}

A Notifier class is declared which stores some global internal data (a boolean value in this case) and then used in the child and parent component.

The child component reacts to the changes by listening to them and invoking one of its functions, while the parent sets the changes in motion by altering the state of the notifier.

Conclusion

Invoking functions in child components is sometimes a necessary process which thankfully does not require a lot of setup.

Even though you could use any of these methods, they really depend on the situation and your choice of app architecture.

©   Matija Novosel 2024