Case Study: Externally rebuilding StatefulWidget

benz93chung

benz93chung

Posted on November 4, 2021

Case Study: Externally rebuilding StatefulWidget

Stock image of alphabet blocks that altogether constitute to a word of REBUILDING

If you have been developing apps on Flutter long enough, chances are you've been making use of a state management tool, like Provider, BLoC, MobX, or the like.

If so, there might be a chance you've stumbled upon a certain problem, whereby a StatefulWidget doesn't choose to update itself upon state change.

This is a pretty specific problem. So in this article, we will walk through the problem and its solution via examples.

If you just want to know the summary, feel free to scroll downwards to the Summary section.

Building the Counter App

Let's start off with the Hello World of Flutter app, the Counter App. For this, we will be using MobX as its state management tool.

counter_store.dart

import 'package:mobx/mobx.dart';

part 'counter_store.g.dart';

class CounterStore = _CounterStore with _$CounterStore;

abstract class _CounterStore with Store {
  @observable
  int sum = 0;

  @action
  void increment() {
    sum++;
  }
}
Enter fullscreen mode Exit fullscreen mode

Whether you are familiar with MobX or not, just understand that we have a store that contains a data member of sum, and a method that adds said data member by 1.

my_home_page.dart

class MyHomePage extends StatelessWidget {
  final counterStore = CounterStore();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Counter'),
      ),
      body: Center(
        child: Observer(
          builder: (_) => StatelessCounterBody(
            value: counterStore.sum,
            onPressedAddButton: counterStore.increment,
          ),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

stateless_counter_body.dart

class StatelessCounterBody extends StatelessWidget {
  final int value;
  final GestureTapCallback onPressedAddButton;

  const StatelessCounterBody({
    Key? key,
    required this.value,
    required this.onPressedAddButton,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Text(
          'You have pushed the button this many times:',
        ),
        Text(
          '$value',
          style: Theme.of(context).textTheme.headline4,
        ),
        const Padding(padding: EdgeInsets.all(16.0)),
        FloatingActionButton(
          onPressed: onPressedAddButton,
          tooltip: 'Increment',
          child: Icon(Icons.add),
        ),
      ],
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

This Counter App here is done a bit differently. We've refactored the content as a StatelessWidget with the name of StatelessCounterBody.

From where this StatelessCounterBody is used (my_home_page.dart), once the onPressedAddButton callback is triggered, counterStore.increment() is called to set the store's value. Therefore the wrapping Observer widget will discard the current instance and rebuild a new instance of StatelessCounterBody, where the value of its value parameter is the new value of sum.

Voila! The Counter App works!

Sweet! Works well so far.

Going Reactive, Going Stateful

Messi in his usual self, being
Now comes a thought, of making a StatefulWidget version of StatelessCounterBody, and we want to call it as StatefulCounterBody.

Why would we want to do that? That's because now we want to make this widget reactive than static.

And yes, setState shall be used for internal rebuilding upon state changes for this widget in mind, because we do not want it to be reliant on other external means to have its state mutated (for example, expecting a certain store of a certain state management tool as its parameter and mutations are done with it).

stateful_counter_body.dart

class StatefulCounterBody extends StatefulWidget {
  final int initialValue;
  final Function(int) onValueIncrement;

  const StatefulCounterBody({
    Key? key,
    required this.initialValue,
    required this.onValueIncrement,
  }) : super(key: key);

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

class _StatefulCounterBodyState extends State<StatefulCounterBody> {
  late int _value;

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

    _value = widget.initialValue;
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Text(
          'You have pushed the button this many times:',
        ),
        Text(
          '$_value',
          style: Theme.of(context).textTheme.headline4,
        ),
        const Padding(padding: EdgeInsets.all(16.0)),
        FloatingActionButton(
          onPressed: () {
            setState(() {
              _value++;
              widget.onValueIncrement(_value);
            });
          },
          tooltip: 'Increment',
          child: Icon(Icons.add),
        ),
      ],
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

For the case of StatefulCounterBody, we want it to react to the value changes all in itself. From the outside, the initialValue is to be provided, as well as a callback called onValueIncrement, where in itself returns the updated value upon incrementing.

Changed My Mind

So now, we decided that we want to add another button at the home page, where upon click adds the sum by 5.

counter_store.dart

import 'package:mobx/mobx.dart';

part 'counter_store.g.dart';

class CounterStore = _CounterStore with _$CounterStore;

abstract class _CounterStore with Store {
  @observable
  int sum = 0;

  @action
  void increment() {
    sum++;
  }

  // Newly added!
  @action
  void incrementByFive() {
    sum += 5;
  }
}
Enter fullscreen mode Exit fullscreen mode

Hence, adding incrementByFive() to the store.

Making Use Of That StatefulWidget

my_home_page.dart

class MyHomePage extends StatelessWidget {
  final counterStore = CounterStore();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Counter'),
      ),
      body: Center(
        child: Observer(
          builder: (_) => StatefulCounterBody(
            initialValue: counterStore.sum,
            onValueIncrement: (value) {
              counterStore.sum = value;
            },
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: counterStore.incrementByFive,
        tooltip: 'Add by 5',
        child: Icon(Icons.add),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Button added, StatefulCounterBody ready. Let's give it a go.
Nice

Nice! Now let's try the newly added button; bottom right.

Oh no

Wait. Why isn't that working?!

The Fact about StatefulWidgets

Stateless and StatefulWidget diagrams
Diagram by Flutter Clutter. Check out his article about StatelessWidget vs. StatefulWidget

Whenever a StatefulWidget is constructed, the State first gets created via createState(), then the StatefulWidget is built. As the state changes, while the StatefulWidget gets discarded and rebuilt, (internally via setState() or externally via Observer, BlocBuilder, etc.), the State remains.

For our case, as counterStore.incrementByFive() was called and the Observer tries to externally rebuild a new instance of StatefulCounterBody, the instance was rebuilt, but the State persisted.

That is because we are yet to add something that informs the StatefulWidget to look out for the differences between the old and the new instances that should affect the state.

Art Thou Updated?

@override
void didUpdateWidget(covariant StatefulCounterBody oldWidget) {
  super.didUpdateWidget(oldWidget);

  if (oldWidget.initialValue != widget.initialValue) {
    _value = widget.initialValue;
  }
}
Enter fullscreen mode Exit fullscreen mode

Therefore, the lifecycle method we can make use of is didUpdateWidget().

If there's a difference between the previous instance and the new instance, then it should update the state with that of the new instance accordingly.

Oh no

Brilliant! It now works! :D

Summary

If you have problems trying to externally rebuild a StatefulWidget with Observer, BlocBuilder or by however rebuilding is done in other state management tools, then the widget has to know how to be aware of updating its State accordingly if there's found to be differences between the old and the new instance by using the didUpdateWidget() lifecycle method.

💖 💪 🙅 🚩
benz93chung
benz93chung

Posted on November 4, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related