无夜之星辰
无夜之星辰

Reputation: 6158

How to get StatefulWidget's state?

I am new to flutter and the way I get StatefulWidget's state is add a state property to widget, eg:

// ignore: must_be_immutable
class _CustomContainer extends StatefulWidget {
  _CustomContainer({Key key}) : super(key: key);

  @override
  __CustomContainerState createState() {
    state = __CustomContainerState();
    return state;
  }

  __CustomContainerState state;

  void changeColor() {
    if (state == null) return;
    // call state's function
    this.state.changeColor();
  }
}

class __CustomContainerState extends State<_CustomContainer> {
  var bgColor = Colors.red;

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 200,
      height: 200,
      color: bgColor,
    );
  }

  void changeColor() {
    setState(() {
      bgColor = Colors.blue;
    });
  }
}

usage:

final redContainer = _CustomContainer();
final button = FlatButton(
      onPressed: () {
        // call widget function
        redContainer.changeColor();
      },
      child: Text('change color'),
    );

It works, but I wonder is there any hidden danger?

Upvotes: 4

Views: 4189

Answers (3)

无夜之星辰
无夜之星辰

Reputation: 6158

Long time gone and it's time to share my knowledge.

GlobalKey is very useful in this case.

You can get the state of a Widget via it's GlobalKey.

For example there is a RedContainer with a method changeWidth:

class RedContainer extends StatefulWidget {
  const RedContainer({Key? key}) : super(key: key);

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

class RedContainerState extends State<RedContainer> {
  double _width = 100;

  @override
  Widget build(BuildContext context) {
    return Container(
      width: _width,
      height: 100,
      color: Colors.red,
    );
  }

  void changeWidth() {
    setState(() {
      _width = 200;
    });
  }
}

Then there is a page with this RedContainer:

class GlobalKeyDemoPage extends StatelessWidget {
  GlobalKeyDemoPage({Key? key}) : super(key: key);

  final _redKey = GlobalKey<RedContainerState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('global key')),
      body: SingleChildScrollView(
        child: Column(
          children: [
            RedContainer(key: _redKey),
            ElevatedButton(
              onPressed: () {
                _redKey.currentState?.changeWidth();
              },
              child: Text('change RedContainer width'),
            ),
          ],
        ),
      ),
    );
  }
}

As you see I pass a GlobalKey to RedContainer then I can get it's state very easily:

_redKey.currentState;

And call it's public method:

_redKey.currentState?.changeWidth();

Upvotes: 4

Baker
Baker

Reputation: 28010

You'll notice it's very awkward to manipulate Flutter widgets in an imperative fashion, like in the question. This is because of the declarative approach Flutter has taken to building screens.

Declarative vs. Imperative

The approach / philosophy of Flutter UI is a declarative UI vs. an imperative UI.

The example in the question above leans toward an imperative approach.

  • create an object
  • object holds state (information)
  • object exposes method
  • use method to impose change on object → UI changes

A declarative approach:

  • there is state (information) above your object
  • your object is declared (created) from that state
  • if the state changes...
  • your object is recreated with the changed state

Below I've tried to convert the imperative approach above, into a declarative one.

CustomContainer is declared with a color; state known / kept outsideCustomContainer & used in its construction.

After construction, you cannot impose a color change on CustomContainer. In an imperative framework you would expose a method, changeColor(color) and call that method and the framework would do magic to show a new color.

In Flutter, to change color of CustomContainer, you declare CustomContainer with a new color.

import 'package:flutter/material.dart';

/// UI object holds no modifiable state.
/// It configures itself once based on a declared color.
/// If color needs to change, pass a new color for rebuild
class CustomContainer extends StatelessWidget {
  final Color color; 

  CustomContainer(this.color);

  @override
  Widget build(BuildContext context) {
    return Container(
      color: color,
      child: Text('this is a colored Container'),
    );
  }
}

/// A StatefulWidget / screen to hold state above your UI object
class DeclarativePage extends StatefulWidget {
  @override
  _DeclarativePageState createState() => _DeclarativePageState();
}

class _DeclarativePageState extends State<DeclarativePage> {
  var blue = Colors.blueAccent.withOpacity(.3);
  var red = Colors.redAccent.withOpacity(.3);
  
  Color color;
  // state (e.g. color) is held in a context above your UI object
  
  @override
  void initState() {
    super.initState();
    color = blue;
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Declarative Page'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            CustomContainer(color),
            // color "state" ↑ is passed in to build/rebuild your UI object 
            RaisedButton(
              child: Text('Swap Color'),
                onPressed: () {
                  setState(() {
                    toggleColor();
                  });
                }
            )
          ],
        ),
      ),
    );
  }

  void toggleColor() {
    color = color == blue ? red : blue;
  }
}

Read more on declarative vs imperative on Flutter.dev.

setState() Rebuilds & Performance

Performance-wise it seems wasteful to rebuild the entire screen when a single widget, way down in the widget tree needs rebuilding.

When possible (and sensible) it's better to wrap the particular elements that have state & need rebuilding in a StatefulWidget, rather than wrapping your entire page in a StatefulWidget and rebuilding everything. (Likely, this wouldn't even be a problem, I'll discuss further below.)

Below I've modified the above example, moving the StatefulWidget from being the entire DeclarativePage, to a ChangeWrapper widget.

ChangeWrapper will wrap the CustomContainer (which changes color).

DeclarativePage is now a StatelessWidget and won't be rebuilt when toggling color of CustomContainer.

import 'package:flutter/material.dart';

class ChangeWrapper extends StatefulWidget {
  @override
  _ChangeWrapperState createState() => _ChangeWrapperState();
}

class _ChangeWrapperState extends State<ChangeWrapper> {
  final blue = Colors.blueAccent.withOpacity(.3);
  final red = Colors.redAccent.withOpacity(.3);

  Color _color; // this is state that changes

  @override
  void initState() {
    super.initState();
    _color = blue;
  }
  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        CustomContainer(_color),
        RaisedButton(
            child: Text('Swap Color'),
            onPressed: () {
              setState(() {
                toggleColor();
              });
            }
        )
      ],
    );
  }

  void toggleColor() {
    _color = _color == blue ? red : blue;
  }
}

/// UI object holds no state, it configures itself once based on input (color).
/// If color needs to change, pass a new color for rebuild
class CustomContainer extends StatelessWidget {
  final Color color;

  CustomContainer(this.color);

  @override
  Widget build(BuildContext context) {
    return Container(
      color: color,
      child: Text('this is a colored Container'),
    );
  }
}

class DeclarativePage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    print('Declarative Page re/built');
    return Scaffold(
      appBar: AppBar(
        title: Text('Declarative Page'),
      ),
      body: Center(
        child: ChangeWrapper(),
      ),
    );
  }
}

When running this version of the code, only the ChangeWrapper widget is rebuilt when swapping colors via button press. You can watch the console output for "Declarative Page re/built" which is written to debug console only once upon first screen build/view.

If DeclarativePage was huge with hundreds of widgets, isolating widget rebuilds in the above manner could be significant or useful. On small screens like in the first example at top or even or average screens with a couple dozen widgets, the difference in savings are likely negligible.

Flutter was designed to operate at 60 frames per second. If your screen can build / rebuild all widgets within 16 milliseconds (1000 milliseconds / 60 frames = 16.67 ms per frame), the user will not see any jankiness.

When you use animations, those are designed to run at 60 frames (ticks) per second. i.e. the widgets in your animation will be rebuilt 60 times each second the animation runs.

This is normal Flutter operation for which it was designed & built. So when you're considering whether your widget architecture could be optimized it's useful to think about its context or how that group of widgets will be used. If the widget group isn't in an animation, built once per screen render or once per human button tap... optimization is likely not a big concern.

A large group of widgets within an animation... likely a good candidate to consider optimization & performance.

Btw, this video series is a good overview of the Flutter architecture. I'm guessing Flutter has a bunch of hidden optimizations such as Element re-use when a Widget hasn't materially/substantially changed, in order to save on CPU cycles instantiating, rendering & positioning Element objects.

Upvotes: 8

Janvi Patel
Janvi Patel

Reputation: 252

add setState() method where you want to add state

Upvotes: -3

Related Questions