16

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
  • 1
    Can you provide some code ? Commented Jan 13, 2019 at 9:30
  • It will be easier if you can add your code Commented Jan 13, 2019 at 9:35

2 Answers 2

7

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,
      ),
    );
  }
}
Sign up to request clarification or add additional context in comments.

3 Comments

agree with this one. I wasn't aware rebuilding widgets was cheaper for Flutter. On a side note, can you tell me what happens to the widgets we replace with new ones (in this case stars) i.e. do they get disposed or stick around in memory?
If you look at the documentation and different presentations you will learn that Flutter has a tree of Elements which holds the real Flutter application and that the widgets and the widget tree are just the configuration or blueprint for those elements. The widgets are inmutable, so to convey information to the element tree you must create new widgets each time. But the Elements are mutable and they can be updated with the new widget configuration. If the widget is of the same type and the same key as the previous one, then the element is just updated and not removed...
So the widgets are disposed and the elements stay in memory (if same type). I have added an override above for the createElement() method of the StatelessWidget. Run it and you will see that the "Element created" message appears just the first time. But no every time you rebuild the widgets when clicking the stars. On subsequent clicks the elements are just updated.
1

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();
      },
    );
  }
}

1 Comment

it works but what I wanted was to modify the stars in place. I don't think we should create new star objects as new ratings are selected as modifying the star in place sounds like a more efficient thing to do. Any idea how I could do that? Thanks!

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.