iRyanBell

Learning How To Flutter - Part 6

May 22, 2019

Learning How To Flutter - Part 6

Taking a step back for a minute to rethink my approach to app layout/state architecture, I pulled up the sample hello_world starter project from Flutter, which constructs a MaterialApp by nesting extended class constructors:

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

I was curious to see what this would look like without the classes, using functions instead:

import 'package:flutter/material.dart';

StatefulWidget buildHomePage({String title, int counterValue=0}) => StatefulBuilder(
  builder: (BuildContext ctx, StateSetter setState) {
    void incrementCounter() => setState(() => counterValue++);

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$counterValue',
              style: Theme.of(ctx).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
);

final StatefulWidget myHomePage = buildHomePage(title: 'Flutter Demo Home Page');

final MaterialApp myApp = MaterialApp(
  title: 'Flutter Demo',
  theme: ThemeData(
    primarySwatch: Colors.blue,
  ),
  home: myHomePage,
);

void main() => runApp(myApp);

The former is over 50 lines of code, while the second version is closer to 40.

Now, what I really wanted to explore is a streaming model of reactive state. Dart provides a StreamController in the dart:async package that provides sinks and multi-listener broadcasts without the need for rxdart.

You can imagine this as a digital pipeline, where data is pushed into the pipe through an open sink. Outlets can be opened and closed along the pipe and observe the data in transit. We can stop, pause, and resume the stream through a stream controller, or even place a stream transformer somewhere in the middle of the pipeline to transform the data.

I wanted to see what the code would look like using a global state model defined at launch. State could be represented as a tree Map of nested StreamController objects set to broadcast() mode, next to initial values. Then, I could create StatefulWidget components where setState would be triggered by data flowing through a streamCondition gate.

As an example, I created this main.dart file with the aforementioned global state store. Each key has an initial value and a StreamController.

In this example, there’s just one key hello with an initial value of Sup?

main.dart

import 'package:flutter/material.dart';
import 'dart:async';
import './home.dart';

final Map<String,List> streamCtrls = {
  'hello': [
    'Sup?',
    StreamController.broadcast(),
  ],
};

final MaterialApp myApp = MaterialApp(
  title: 'Flutter Demo',
  theme: ThemeData(
    primarySwatch: Colors.blue,
  ),
  home: buildHomePage(
    title: 'Hello World!',
    streamCtrls: streamCtrls,
  ),
);

void main() => runApp(myApp);

The home page builder takes a title and a state stream controller store, then creates an AppBar, a TextInput field, a Text display, and a FloatingActionButton.

home.dart

import 'package:flutter/material.dart';
import './app_bar.dart';
import './attn_text.dart';

Widget buildHomePage({String title, Map streamCtrls}) => Builder(
  builder: (BuildContext ctx) => Scaffold(
    appBar: PreferredSize(
      preferredSize: Size(double.infinity, 50.0),
      child: buildAppBar(
        streamCtl: streamCtrls['hello'],
        streamCond: (data) => data.endsWith('?'),
      ),
    ),
    body: Container(
      padding: const EdgeInsets.all(32.0),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          TextField(
            onChanged: (String val) => streamCtrls['hello'][1].sink.add(val),
          ),
          buildAttnText(
            streamCtl: streamCtrls['hello'],
            streamCond: (data) => data.endsWith('!'),
          ),
        ],
      ),
    ),
    floatingActionButton: FloatingActionButton(
      onPressed: () => streamCtrls['hello'][1].sink.add('Wow!'),
      tooltip: 'Announcement',
      child: Icon(Icons.announcement),
    ),
  ),
);

State FLOWS through our widget builder’s conditional gate.

build.dart

import 'package:flutter/material.dart';

StatefulWidget build({
  List streamCtl,
  Function streamCond,
  Function builder}) => StatefulBuilder(
  builder: (BuildContext ctx, StateSetter setState) {
    if (streamCtl != null) {
      streamCtl[1].stream.listen(
        (data) {
          if (streamCond == null || streamCond(data)) {
            setState(() => streamCtl[0] = data);
          }
        }
      );
    }

    return builder(ctx: ctx, val: streamCtl == null ? null : streamCtl[0]);
  },
);

Each component listens to the stream hello, but interprets the data slightly differently.

app_bar.dart

import 'package:flutter/material.dart';
import './build.dart';

StatefulWidget buildAppBar({List streamCtl, Function streamCond}) => build(
  streamCtl: streamCtl,
  streamCond: streamCond,
  builder: ({dynamic val, BuildContext ctx}) => AppBar(
    title: Text(val),
  ),
);

The app bar updates when the form input ends with a question mark.

attn_text.dart

import 'package:flutter/material.dart';
import './build.dart';

StatefulWidget buildAttnText({List streamCtl, Function streamCond}) => build(
  streamCtl: streamCtl,
  streamCond: streamCond,
  builder: ({dynamic val, BuildContext ctx}) => Text(
    'Attention: ${ streamCtl[0] }',
  ),
);

Meanwhile, the attention text updates when the stream value ends with an exclamation point.

The floating action button updates the value to Wow!

What’s interesting about this model, is that we’ve decoupled the nested layout structure of the components from the nested structure of the state. Components are able to listen to any state streams of their choosing, then decide if the change is sufficient to render.

demo

What could we call this model? Functional Listener/Observable Widgets (FLOWs).

You can download the project here: 👇
https://github.com/iRyanBell/learning-how-to-flutter_gsdist-part-6