Flutter Set value from parent widget

eye-catch Dart and Flutter

I wanted to call a function from the parent widget to clear the current value of the child widget. To do that, we need to use GlobalKey but I implemented it without it at first. However, Dart compiler complains about it because a mutable variable exists in StatefulWidget.
I will explain how to use GlobalKey in this article.

Sponsored links

Explaining use case

I defined the following interface.

abstract class ValueController extends StatefulWidget{
  void clear();
  double get value;
}

This is used for several widgets because I want to have count widget, input widget, dropdown widget, and rating widget. When I touch the cancel button it clears those values. When I touch the ok button it read them and inserts/updates data in the database.
The example view looks like the following.

The items are located in ListView and each item is ListTile. The contents of the view depend on entries stored in a database. Each ListTile is used in the following widget. What I want to do is to call clear function and getValue in this widget.

class ItemWidget extends StatelessWidget {
  final Item item;
  late final ValueController _widget;

  ItemWidget(this.item, this.setStateCallback) {
    _widget = _getTrailing(item.type);
  }

  @override
  Widget build(BuildContext context) {
    ...
  }

  ValueController _getTrailing(int type) {
    if (type == ValueType.amount.value) {
      return SizeConstrainedNumber(key: _key);
    } else if (type == ValueType.amountForAlcohol.value) {
      return SizeConstrainedNumber(key: _key);
    } else if (type == ValueType.number.value) {
      return CountWidget(key: _key);
    } else if (type == ValueType.rating.value) {
      return RatingBarWidget(key: _key);
    } else {
      throw ("Unknown type [$type]");
    }
  }

  // This is what I want
  void clear(BuildContext context) {
    _widget.clear();
  }

  // This is what I want
  double getValue() {
    return _widget.value;
  }
}
Sponsored links

The problem using late keyword in StatefulWidget

As I mentioned above, I have several widgets that extend ValueController interface. The following widget is one of them. The following code is my first implementation.

class CountWidget extends ValueController {
  late final _CountWidget _widget;

  /// [min] must be 0 or smaller. Default is -99.
  /// [max] must be 0 or bigger. Default is 99.
  CountWidget({min = -99, max = 99}) {
    this._widget = _CountWidget(min, max);
  }

  @override
  State<StatefulWidget> createState() => _widget;
  double get value => _widget._count.toDouble();
  void clear() {
    _widget.clear();
  }
}

To call clear function or access to the getter from outside we need to define them in CountWidget which is actually used from outside. It means we need to have the instance of _CountWidget. It looks good at first glance for a beginner like me. _widget won’t be null but createState must return a new instance. It violates the constraints of createState function. This doesn’t cause any problem in the example above but it causes an error in another case. Let’s see another case that causes an error in the next chapter.

Widget returned an old or invalid state instance error when scrolling

I got the following error message when scrolling a list view.

════════ Exception caught by widgets library ═══════════════════════════════════
The createState function for CategoryDropdown returned an old or invalid state instance: CategoryDropdown, which is not null, violating the contract for createState.
'package:flutter/src/widgets/framework.dart':
Failed assertion: line 4681 pos 7: 'state._widget == null'

The relevant error-causing widget was
Expanded
lib\AddCategoryView.dart:51

The error message appears on the screen as well.

The code for the view is following.

class _AddCategoryView extends State<AddCategoryView> {
...    // other code 
    var listView = ListView.builder(
      itemCount: _categories.length,
      itemBuilder: (context, index) {
        return Container(
            margin: EdgeInsets.all(5),
            child: Row(
              children: [
                Expanded(flex: 2, child: Center(child: _categories[index])),
                Expanded(flex: 2, child: Center(child: _dropdowns[index])),
              ],
            ));
      },
    );
... // other code
    return SafeArea(
      child: Scaffold(
          appBar: AppBar(
            title: Text("Add Category"),
          ),
          body: Column(
            children: [
              addTile,
              columnTitles,
              Expanded(child: listView),
              okButtonRow,
            ],
          )),
    );
}

I thought it was something wrong with the list view implementation at first because the error message says Expanded widget in the list view caused the error. My thought was the height of the list view increased when touching the plus button and its size exceeds the given size. I tried to use Flexible widget instead of Expanded but it didn’t work because it’s basically the same. It doesn’t change anything since expanding direction is horizontal direction although the error happens when scrolling in vertical direction.

The root cause lurks in my own dropdown widget.

class CategoryDropdown extends ValueController {
  late _CategoryDropdown _widget;
  CategoryDropdown() {
    _widget = _CategoryDropdown();
  }

  @override
  void clear() {
    _widget._categoryValue = 0;
    _widget._categoryController.text = "";
  }

  @override
  double get value => _widget._categoryValue.toDouble();

  @override
  _CategoryDropdown createState() => _widget;
}

createState function seems to be called multiple times while the class instance is alive. When it’s called NEW INSTANCE must be created in the function.

Subclasses should override this method to return a newly created instance of their associated State subclass:

https://api.flutter.dev/flutter/widgets/StatefulWidget/createState.html

I didn’t know that. Then, the fix is easy. Let’s create a new instance in createState function.

class CategoryDropdown extends ValueController {
  late _CategoryDropdown _widget;

  @override
  void clear() {
    _widget._categoryValue = 0;
    _widget._categoryController.text = "";
  }

  @override
  double get value => _widget._categoryValue.toDouble();

  @override // create new instance here
  _CategoryDropdown createState() => _widget = _CategoryDropdown();
}

It works.

However, all variables defined in StatefulWidget must be final. ValueController extends StatefulWidget, so we need to follow the constraints in the widget as well.

Access members in State via GlobalObjectKey

I couldn’t find a good way to set value in State class via StatefulWidget. It is I guess bad practice. Instead of defining functions in StatefulWidget, GlobalKey can be used for it. However, my ItemWidget is StatelessWidget and it’s impossible to use GlobalKey but we can use GlobalObjectKey.

The ItemWidget looks like the following.

class ItemWidget extends StatelessWidget {
  final Item item;
  final Function setStateCallback;
  late final Widget _widget;
  late final GlobalObjectKey<ValueControllerState> _key;

  ItemWidget(this.item, this.setStateCallback) {
    _widget = _getTrailing(item.type);
  }

  @override
  Widget build(BuildContext context) {
    ...
  }

  Widget _getTrailing(int type) {
    // Specify an unique key 
    _key = GlobalObjectKey(item.name);
    if (type == ValueType.amount.value) {
      return SizeConstrainedNumber(key: _key);
    } else if (type == ValueType.amountForAlcohol.value) {
      return SizeConstrainedNumber(key: _key);
    } else if (type == ValueType.number.value) {
      return CountWidget(key: _key);
    } else if (type == ValueType.rating.value) {
      return RatingBarWidget(key: _key);
    } else {
      throw ("Unknown type [$type]");
    }
  }

  void clear(BuildContext context) {
    _key.currentState?.clear();
  }

  double getValue() {
    assert(_key.currentState != null);
    return _key.currentState!.value;
  }
}

By setting a key, we can access the target state instance but be careful that the data type is nullable.

We no longer need clear function and value getter in CategoryDropdown. Instead, we need them in State class.

abstract class ValueControllerState<T extends StatefulWidget> extends State<T> {
  void clear();
  double get value;
}

class CategoryDropdown extends StatefulWidget{
  @override
  _CategoryDropdown createState() => _CategoryDropdown();
}

class _CategoryDropdown extends ValueControllerState<CategoryDropdown> {
  TextEditingController _categoryController = TextEditingController();
  int _categoryValue = 0;

  @override
  void clear() {
     _categoryValue = 0;
    _categoryController.text = "";
  }

  @override
  double get value => _categoryValue.toDouble();
...
}

Comments

Copied title and URL