Flutter change state from StatefulWidget object

Viewed 19238

Like title states, how can one access the state of a StatefulWidget from the StatefulWidget.

Background: I have a star rating widget that consists of 5 "StarWidget"s in a row. The StarWidget class is just an Icon with a detector wrapped around it (not using IconButton because it has a very large size). The StarWidget stores whether it is selected or not in a corresponding State object and accordingly displays a solid or outline icon.

In my main widget, I have access to the StatefulWidget objects, and would like to configure their states.

import 'package:flutter/material.dart';

import 'package:font_awesome_flutter/font_awesome_flutter.dart';

class StarRatingWidget extends StatefulWidget {
  @override
  _StarRatingWidgetState createState() {
    return _StarRatingWidgetState();
  }
}

class _StarRatingWidgetState extends State<StarRatingWidget>
    implements StarSelectionInterface {
  //Properties
  int _currentRating = 0;
  List<RatingStarWidget> starWidgets = [];

  //Methods
  @override
  void initState() {
    super.initState();
    starWidgets.add(
      RatingStarWidget(
        starSelectionInterface: this,
        starPosition: 0,
      ),
    );
    starWidgets.add(
      RatingStarWidget(
        starSelectionInterface: this,
        starPosition: 1,
      ),
    );
    starWidgets.add(
      RatingStarWidget(
        starSelectionInterface: this,
        starPosition: 2,
      ),
    );
    starWidgets.add(
      RatingStarWidget(
        starSelectionInterface: this,
        starPosition: 3,
      ),
    );
    starWidgets.add(
      RatingStarWidget(
        starSelectionInterface: this,
        starPosition: 4,
      ),
    );
  }

  @override
  Widget build(BuildContext buildContext) {
    return Row(
      children: starWidgets,
    );
  }

  //Star Selection Interface Methods
  void onStarSelected(_RatingStarWidgetState starWidgetState) {
    print("listener: star selected ${starWidgetState._starPosition}");

    //a new, rating has been selected, update rating
    if (_currentRating != starWidgetState._starPosition) {
      _currentRating = (starWidgetState._starPosition + 1);
    }

    //same star as rating has been selected, set rating to 0
    else {
      _currentRating = 0;
    }

    //update stars according to rating
    for(int i = 1; i <= 5; i++) {
      //what should I do here?!
    }
  }
}

class RatingStarWidget extends StatefulWidget {
  //Properties
  final int starPosition;
  final StarSelectionInterface starSelectionInterface;

  //Constructors
  RatingStarWidget({this.starSelectionInterface, this.starPosition});

  //Methods
  @override
  _RatingStarWidgetState createState() {
    return _RatingStarWidgetState(starSelectionInterface, starPosition);
  }
}

class _RatingStarWidgetState extends State<RatingStarWidget> {
  //Properties
  int _starPosition;
  bool _isSelected = false;
  StarSelectionInterface selectionListener;

  //Constructors
  _RatingStarWidgetState(this.selectionListener, this._starPosition);

  //Methods
  @override
  Widget build(BuildContext buildContext) {
    return AnimatedCrossFade(
      firstChild: GestureDetector(
        child: Icon(
          FontAwesomeIcons.star,
          size: 14,
        ),
        onTap: () {
          print("star: selected");
          selectionListener.onStarSelected(this);
        },
      ),
      secondChild: GestureDetector(
        child: Icon(
          FontAwesomeIcons.solidStar,
          size: 14,
        ),
        onTap: () {
          selectionListener.onStarSelected(this);
        },
      ),
      duration: Duration(milliseconds: 300),
      crossFadeState:
          _isSelected ? CrossFadeState.showSecond : CrossFadeState.showFirst,
    );
  }
}

class StarSelectionInterface {
  void onStarSelected(_RatingStarWidgetState starWidgetState) {}
}
2 Answers

The Flutter way is to rebuild widgets whenever it is necessary. Don't be afraid to build widgets, they are cheap for the SDK, specially in this case for simple stars.

Accessing another widget state requires more work than just rebuilding it. To access the state you should use keys or you should add special methods in the widget itself.

In this case, where the star is rebuilt no matter what, it is even better and simpler to use plain stateless widgets because the selected state can be provided by the parent in the moment of rebuilding.

And since the state is stored in the parent widget, I think it is better no to store it as wall in each one of the individual stars.

Next is a very simple solution that follows that idea. But yes, it still rebuilds the stars.

import 'package:flutter/material.dart';

import 'package:font_awesome_flutter/font_awesome_flutter.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(body: Center(child: StarRatingWidget())),
    );
  }
}

class StarRatingWidget extends StatefulWidget {
  @override
  _StarRatingWidgetState createState() {
    return _StarRatingWidgetState();
  }
}

class _StarRatingWidgetState extends State<StarRatingWidget> {
  int _currentRating = 0;

  List<Widget> buildStars() {
    List<RatingStarWidget> starWidgets = [];
    for (int i = 0; i < 5; i++) {
      starWidgets.add(
        RatingStarWidget(
          clickCallback: () => setState(() {
                _currentRating = i + 1;
              }),
          highlighted: _currentRating > i,
        ),
      );
    }
    return starWidgets;
  }

  @override
  Widget build(BuildContext buildContext) {
    return Row(
      children: buildStars(),
    );
  }
}

class RatingStarWidget extends StatelessWidget {
  //Properties
  final VoidCallback clickCallback;
  final bool highlighted;

  //Constructors
  RatingStarWidget({this.clickCallback, this.highlighted});

  @override
  StatelessElement createElement() {
    print("Element created");
    return super.createElement();
  }

  //Methods
  @override
  Widget build(BuildContext buildContext) {
    return GestureDetector(
      onTap: () {
        clickCallback();
      },
      child: AnimatedCrossFade(
        firstChild: Icon(
          FontAwesomeIcons.star,
          size: 14,
        ),
        secondChild: Icon(
          FontAwesomeIcons.solidStar,
          size: 14,
        ),
        duration: Duration(milliseconds: 300),
        crossFadeState:
            highlighted ? CrossFadeState.showSecond : CrossFadeState.showFirst,
      ),
    );
  }
}

I wrote my own example similar to yours. What I do here is:

Initial star rate is -1 because arrays start from 0 ;) and I create stars with position, current star rate and the callback function. We will use this callback function to update the value in the ScreenOne.

In Star widget, we have a local bool selected with default value false and we assign it a value inside the build function based on the position of the star and current rate. And we have setSelected() function which runs the callback function and updates currentRate with the value of star position.

Check the video example here.

class ScreenOne extends StatefulWidget {
  @override
  _ScreenOneState createState() => _ScreenOneState();
}

class _ScreenOneState extends State<ScreenOne> {
  int currentRate = -1; //since array starts from 0, set non-selected as -1
  List<Star> starList = []; //empty list
  @override
  void initState() {
    super.initState();
    buildStars(); //build starts here on initial load
  }

  Widget buildStars() {
    starList = [];
    for (var i = 0; i < 5; i++) {
      starList.add(Star(
        position: i,
        current: currentRate,
        updateParent: refresh, //this is callback
      ));
    }
  }

  refresh(int index) {
    setState(() {
      currentRate = index; //update the currentRate
    });
    buildStars(); //build stars again
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      appBar: AppBar(
        title: Text("Test page 1"),
      ),
      body: Container(
        child: Center(
          child: Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: starList,
          ),
        ),
      ),
    );
  }
}






class Star extends StatefulWidget {
  final Function(int index) updateParent; //callback
  final int position; //position of star
  final int current; //current selected star from parent

  const Star({Key key, this.position, this.updateParent, this.current})
      : super(key: key);

  @override
  _StarState createState() => _StarState();
}

class _StarState extends State<Star> {
  bool selected = false;

  void setSelected() {
    widget.updateParent(widget.position);
  }

  @override
  Widget build(BuildContext context) {
    if (widget.current >= widget.position) {
      selected = true;
    } else {
      selected = false;
    }
    return GestureDetector(
      child: AnimatedCrossFade(
        firstChild: Icon(Icons.star_border),
        secondChild: Icon(Icons.star),
        crossFadeState:
            selected ? CrossFadeState.showSecond : CrossFadeState.showFirst,
        duration: Duration(milliseconds: 300),
      ),
      onTap: () {
        setSelected();
      },
    );
  }
}
Related