Widget Communication with Flutter using VoidCallback and Function(x)

Paul Halliday

In this article we’re going to investigate how we can use callback-style events to communicate between widgets with Flutter.

Why is this important? It allows us to separate our widgets into small, testable units that can be adaptable to their context.

Creating a New Flutter Project

As always, we’ll start off by setting up a new project:

# New Flutter project
$ flutter create widget_communication

# Open this up inside of VS Code
$ cd widget_communication && code .

We can now open this up in the iOS or Android simulator from within VS Code.

Video Version

If you’re interested, I’ve recorded a video where you can see me go over the steps in this article:

Count This!

The first method we’re going to use is simply passing data down to the child as a property. Let’s update main.dart to contain a reference to our CounterPage that we’ll create in a second:

import 'package:flutter/material.dart';
import 'package:widget_communication/counter_page.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Widget Communication',
      home: CounterPage(),
    );
  }
}

The CounterPage widget is a simple StatefulWidget:

// counter_page.dart
import 'package:flutter/material.dart';
import 'package:widget_communication/count.dart';

class CounterPage extends StatefulWidget {
  _CounterPageState createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int count = 0;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Widget Communication")),
      body: Center(
        child: Count(count),
      ),
    );
  }
}

Inside of this widget we’re establishing a count equal to 0 and passing this into a widget named Count as a property. Let’s create the Count widget:

// count.dart
import 'package:flutter/material.dart';

class Count extends StatelessWidget {
  final int count;

  Count(this.count);

  @override
  Widget build(BuildContext context) {
    return Text("$count");
  }
}

This gives us the following expected count of 0:

Widget prop

VoidCallback

For example’s sake, let’s turn our count into a Button and say that any time we click the button we want to notify the parent CounterPage.

As we don’t want to return a value here, we’ll need to register a VoidCallback. We’ll also add braces to the items within our Count constructor to make them named parameters.

// count.dart
class Count extends StatelessWidget {
  final int count;
  final VoidCallback onCountSelected;

  Count({@required this.count, this.onCountSelected});

  @override
  Widget build(BuildContext context) {
    return FlatButton(
      child: Text("$count"),
      onPressed: () => onCountSelected(),
    );
  }
}

We’ll then need to update our CounterPage to listen to the onCountSelected callback:

// counter_page.dart
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text("Widget Communication")),
    body: Center(
      child: Count(
        count: count,
        onCountSelected: () {
          print("Count was selected.");
        },
      ),
    ),
  );
}

If we select the value of our counter now, we should see Count was selected. inside of the debug console!

Function(x)

Whilst the use of VoidCallback is great for identifying callback events with no expected value, what do we do when we want to return a value back to the parent?

Enter, Function(x):

// counter_page.dart
import 'package:flutter/material.dart';

class Count extends StatelessWidget {
  final int count;
  final VoidCallback onCountSelected;

  final Function(int) onCountChange;

  Count({
    @required this.count,
    @required this.onCountChange,
    this.onCountSelected,
  });

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        IconButton(
          icon: Icon(Icons.add),
          onPressed: () {
            onCountChange(1);
          },
        ),
        FlatButton(
          child: Text("$count"),
          onPressed: () => onCountSelected(),
        ),
        IconButton(
          icon: Icon(Icons.remove),
          onPressed: () {
            onCountChange(-1);
          },
        ),
      ],
    );
  }
}

Here we’ve added a couple of buttons and a new Function(int) named onCountChange that we’re calling with the value that we want to pass back to the parent.

Inside of the parent we’re able to listen to this and change the value of count accordingly:

class _CounterPageState extends State<CounterPage> {
  int count = 0;
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Widget Communication")),
      body: Center(
        child: Count(
          count: count,
          onCountSelected: () {
            print("Count was selected.");
          },
          onCountChange: (int val) {
            setState(() => count += val);
          },
        ),
      ),
    );
  }
}

Here’s the result of our work:

Widget communication

  Tweet It

🕵 Search Results

🔎 Searching...

Sponsored by #native_company# — Learn More
#native_title# #native_desc#
#native_cta#