Communication in Widget

Communication between widget means passing data from one widget to another widget. If you build a custom widget you might wanna make the widget in a way that it would change its appearence and behaviour based on different data is given to it. For example, if you build a Widget that shows a clock, this Clock widget may have a size property that takes a dimension. On different dimention the clock might look smaller or bigger. You can configure the Widget in that way so that the Widget would change its size based on the Dimension provided to it. On the other hand the Clock widget might give the parent widget a signal that the time has changed. The Clock widget might pass some data to its parent widget on time to time or on some user interaction. Thus providing data from parent widget to child widget and receiving the data from child widget to parent widget is called Two Way Communication.

Passing Data to a Stateless Widget

Here we will learn how we can pass data from Parent widget to child widget.

// Represents a Label for the FormField --
class FormLabel extends StatelessWidget{

  String title;

  FormLabel(this.title);

  @override
  Widget build(BuildContext context) {
    return Container(
      alignment : Alignment.centerLeft,
      padding : EdgeInsets.only(bottom: 5.0),
      child : Text(title, style: TextStyle(fontWeight : FontWeight.w500, fontSize : 15)),
    );
  }
}

Here we have created a custom Widget FormLabel and it is a Stateless widget. The constructor has one positional parameter called title which is a required field.

Now a parent widget can use this FormLabel widget with different label to create label with similar look and feel.

Container(
  child: FormLabel("What is your Name?"),
),

Container(
  child: FormLabel("What is your Date of Birth?"),
),

Receiving Data from a Stateless Widget

Here we will learn how we can receive data from Stateless widget.

VoidCallback

// Represents a Label for the FormField --
class FormLabel extends StatelessWidget{

  String title;
  VoidCallback onUserTap;

  FormLabel({this.title, this.onUserTap});

  @override
  Widget build(BuildContext context) {
    return Container(
      alignment : Alignment.centerLeft,
      padding : EdgeInsets.only(bottom: 5.0),
      child : GestureDetector(
        onTap: (){
          onUserTap();
        },
        child: Text(title, style: TextStyle(fontWeight : FontWeight.w500, fontSize : 15)),
      ),
    );
  }
}

The FormLabel class now modified to take a callback onUserTap. But look at the constructor, we have embraced the arguments within the curly brackets. By enclosing them within the curly bracket they become named parameters and they are optional.

We have also put the Text widget within the GestureDetector widget to listen for onTap event. Whenever user taps into the text, it calls onUserTap callback which is provided by the parent. Now a parent widget can provide a callback to the FormLabel widget to listen for tap event.

As the FormLabel widget now made to take named parameters a parent widget must provide argument name to pass the data. Below is an example.

Container(
  title: "What is your name?",
  onUserTap: (){
    // Show a help toast maybe --
  },
),

Also notice that on declaring the onUserTap variable we have used VoidCallback data type. Because as our callback doesn't carray any value we must use this data type. Now we will see how we can pass data through callback.

Function(x)

// Represents a Label for the FormField --
class FormLabel extends StatelessWidget{

  String title;
  Function(int) onUserTap;

  FormLabel({this.title, this.onUserTap});

  @override
  Widget build(BuildContext context) {
    return Container(
      alignment : Alignment.centerLeft,
      padding : EdgeInsets.only(bottom: 5.0),
      child : GestureDetector(
        onTap: (){
          onUserTap(title.length);
        },
        child: Text(title, style: TextStyle(fontWeight : FontWeight.w500, fontSize : 15)),
      ),
    );
  }
}

Now while declaring the onUserTap callback we have used Function(int) syntax. That means we are declaring a callback which will receive a integer value as the first argument. In the build method, we are now sending title.length value to the parent widget. Your parent widget will now be able to receive data from a child widget.

Container(
  title: "What is your name?",
  onUserTap: function(int length){
    pring(length);
  }
),

This way you can pass as many data as you want and any type of data from child widget to its parent widget.

Parent Child Communcation for Stateful Widget

The machanism for communcation for Stateful Widget is simmilar to Stateless widget. Below is an example of a DatePicker class which renders a input field for picking Date.

// DatePicker 
class DatePicker extends StatefulWidget{
  Function(DateTime) onDateSelected;
  bool clearDate;
  DateTime selectedDate;

  DatePicker({this.onDateSelected, this.clearDate, this.selectedDate});

  @override
  _DatePickerState createState() => _DatePickerState();
}
class _DatePickerState extends State<DatePicker>{

  String dateText = "";
  bool dateSelected = false;
  DateTime initialDate = DateTime.now();
  DateTime userSelectedDate;

  @override
  initState(){
    super.initState();

    if(widget.selectedDate == null){
      dateSelected = false;
    }else{
      dateSelected = true;
      userSelectedDate = widget.selectedDate;
      widget.onDateSelected(userSelectedDate);
    }
  }

  Future<Null> _selectDate(BuildContext context) async {
    final DateTime picked = await showDatePicker(
        initialDate: initialDate,
        firstDate: DateTime(2020, 1),
        lastDate: DateTime(2101),
          context: context,
      );
      if (picked != null && picked != userSelectedDate){
        setState(() {
          initialDate = picked;
            userSelectedDate = picked;
            dateSelected = true;
          });

          widget.onDateSelected(userSelectedDate);
      }
  }

  Widget _formatDate(){
    if(dateSelected == true){
      List months = [
                      'January', "February", "March", "April", 
                      "May", "June", "July", "August", 
                      "September", "October", "November", "December"
                    ];
      String month = months[userSelectedDate.month - 1];
      int year = userSelectedDate.year;
      int day = userSelectedDate.day;
      return RichText(
        text: TextSpan(
          text : "$day",
          style: TextStyle(color: Colors.black87),
          children : <TextSpan>[
            TextSpan(text : " $month,", style : TextStyle(fontWeight: FontWeight.w500, color: Colors.black87)),
            TextSpan(text : " $year", style : TextStyle(color: Colors.black87)),
          ]
        )
      );
    }else{
      return Text("");
    }
  }

  void _clearDate(){
    setState(() {
      initialDate = DateTime.now();
      userSelectedDate = null;
      dateSelected = false;
      });

      widget.onDateSelected(null);
  }

  @override
  Widget build(BuildContext context){
    return DecoratedBox(
      decoration: BoxDecoration(
        border: Border.all(width: 2.0, color: const Color.fromRGBO(229, 229, 229, 1)),
        borderRadius: BorderRadius.all(Radius.circular(4.0)),
      ),
      child: SizedBox(
        height: 30,
        child: Padding(
          padding: EdgeInsets.all(5.0),
          child: Row(
            children: <Widget>[
              Expanded(
                child: GestureDetector(
                  onTap: (){
                    _selectDate(context);
                  },
                  child: (dateSelected) ? _formatDate() : Text("Choose a Date", style: TextStyle(fontSize: 12)),
                ),
              ),
              (widget.clearDate) ? Padding(
                padding: EdgeInsets.only(right: 5),
                child: GestureDetector(
                  onTap: (){
                    _clearDate();
                  },
                  child: Icon(
                    Icons.clear, 
                    size: 15.0,
                    color: (dateSelected) ? Colors.black54 : Colors.transparent,
                  ),
                )
              ) : SizedBox(height: 0, width: 0)
            ],
          ),
        ),
      ),
    );
  }
}

In the constructor, we have named parameters, one is callback which is onDateSelected and another two parameters are clearDate and selectedDate.

The callback onDateSelected is declared as Function(DateTime) which means the callback will receive a DateTime value.

The paremeter selectedDate is used to provide a initial date that will be present on the input field.

If clearDate is true, than the input field will render a cross icon at the end of the field. This cross icon will appear when user have selected a date or a initial value is present which is provided through selectedDate property. Upon clicking on the cross icon, the date will be cleared from the input field. If clearDate is false, the icon will not be rendered in any situation.

Here is the usage of the above widget:

DatePicker(
  clearDate : false,
  selectedDate : DateTime.now(),
  onDateSelected: (DateTime date){
    startingDate = date;
  }
),

widget variable

You have declared the class properties within the DatePicker widget class. But how you would access these properties from its State class? Every State class has a widget variable which refers to its widget class. So to access the widget parameters you can use widget variable.

In the above example, look at the _selectDate() method. When this method is called, a date picker is opened for choosing the date. When a user picks a date from the date picker modal, it calls the callback onDateSelected with the selected date passed to the callback. We are accessing this callback method through widget variable.

widget.onDateSelected(userSelectedDate);

@required

In the above examples, all named parameters are optional. That means you still can use the widget without providing any value to it. But it might cause unexpected result cause unprovided parameter will contain null. Your widget must be capable of handling the null value. But what if you want to make a named parameter required? You can use @required annotation in this case.

DatePicker({@required this.onDateSelected, this.clearDate, @required this.selectedDate});

In the above statement, the callback onDateSelected and selectedDate parameter is made required and clearDate is optional parameter. So your parent widget must provide values for these two required parameter.