Introducing var_widget for flutter

What is var_widget?

It’s a new state management scheme based for special but common cases.

It’s extension on stream-based state management based on two assumptions. So, what are those assumptions?

  • Some streams always have known value and can be used by multiple widget without any problem.
  • Some streams simply depends on other stream.

It’s replacement of stream for some special cases, as you might notice from assumptions. So it can be used with BloC pattern.

When I can use it?

You can use var_widget when your stream always has current value or value is computed based on other value. Example usages are for score, and item count.

The challenge

We consider challenge from https://medium.com/flutter-community/flutter-oneyearchallenge-scoped-model-vs-bloc-pattern-vs-states-rebuilder-23ba11813a4f

The UI of the challenging app consists of a horizontal ListView of counter itemCards. When the use taps on any itemCard from the ListView, a card appears in the detailed container under the ListView with the same color and count number.

We start with a new stateful widget with two field.

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  List<Var<int>> vars;
  Var<int> idx;

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

    vars = List.generate(15, (i) => new Var(0, debugLabel: 'Var $i'));
    idx = new Var(0, debugLabel: 'index');
  }
}

Debug labels are printed while using inspector. See inspector for more details.

We now implement build method. We start with listview.

ListView.builder(
  itemCount: vars.length,
  scrollDirection: Axis.horizontal,
  itemBuilder: (BuildContext context, int i) {
      // We use random to ensure that unrelated widgets are not rebuilt.
      final Color randColor = RandomColor().randomColor();

      return Container(
        margin: const EdgeInsets.symmetric(horizontal: 8.0),
        width: 100,
        color: randColor,
        child: GestureDetector(
          child: Card(
              color: randColor,
              child: Center(
                child: VarBuilder<int>(
                  value: vars[i],
                  builder: (context, int v, child) => Text('$v'),
                ),
              ),
          ),
          onTap: () => idx.value = i,
        ),
      );
  },
),

VarBuilder<T>, which extends VarWidget<T>, rocks. Widget which extends VarWidget<T> is rebuilt whenever value of Value<T>.value is changed. Also note that VarBuilder is used at almost leaf level. As the only child of VarBuilder is Text, only Text is rebuilt when vars[i].value is changed.

We now implement click counter.

Center(
  child: VarBuilder<int>(
      value: idx,
      builder: (context, int idx, child) {
        return SizedBox(
          width: 200,
          height: 200,
          child: InkWell(
              child: Card(
                child: Center(
                  child: VarBuilder<int>(
                      value: vars[idx],
                      builder: (context, int v, child) {
                        return Text(
                          '$v',
                          style: Theme.of(context).textTheme.display1,
                        );
                      },
                  ),
                ),
                color: Colors.blueAccent,
              ),
              onTap: () => vars[idx].value++,
          ),
        );
      },
  ),
),

Here, we use two VarBuilders; one for index and one for counter. As VarBuilder is used twice, there’s two scenario of rebuilding.

  • When index is updated

builder: (context, int idx, child) is invoked and child nodes (including SizedBox) are rebuilt. (Note that this can be optimized further.)

  • When value is updated

builder: (context, int v, child) is invoked and only Text node is rebuilt.

We are done. Full build:

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('var_widget'),
      ),
      body: Column(
        children: <Widget>[
          Container(
            padding: EdgeInsets.only(top: 10),
            height: 150,
            child: ListView.builder(
              itemCount: vars.length,
              scrollDirection: Axis.horizontal,
              itemBuilder: (context, int i) {
                final Color randColor = RandomColor().randomColor();

                return Container(
                  margin: const EdgeInsets.symmetric(horizontal: 8.0),
                  width: 100,
                  color: randColor,
                  child: GestureDetector(
                    child: Card(
                      color: randColor,
                      child: Center(
                        child: VarBuilder<int>(
                          value: vars[i],
                          builder: (context, int v, child) => Text('$v'),
                        ),
                      ),
                    ),
                    onTap: () => idx.value = i,
                  ),
                );
              },
            ),
          ),
          Divider(),
          Center(
            child: VarBuilder<int>(
              value: idx,
              builder: (context, int idx, child) {
                return SizedBox(
                  width: 200,
                  height: 200,
                  child: InkWell(
                    child: Card(
                      child: Center(
                        child: VarBuilder<int>(
                          value: vars[idx],
                          builder: (context, int v, child) {
                            return Text(
                              '$v',
                              style: Theme.of(context).textTheme.display1,
                            );
                          },
                        ),
                      ),
                      color: Colors.blueAccent,
                    ),
                    onTap: () => vars[idx].value++,
                  ),
                );
              },
            ),
          ),
        ],
      ),
    );
  }

For full code, see github

Inspecting values with inspector

Thanks to assumptions above, we can know current value of variable while using inspector.

Inspector