Flutter: Bindings? What bindings?

Bindings: Typically used in frameworks that declare the UI in a mark-up language like Xaml that automagically updates widgets and properties in linked ViewModels.

Having worked with Xamarin Forms for 3 years I understand the confusion some get when starting with Flutter. The most disturbing questions you get are

  • Where is my BindingContext
  • How do I make Bindings

The terrible truth is:

There are no Bindings in Flutter

That's because of the way Flutter Pages are created. As Flutter always rebuilds its widgets on data changes there is no way we can define a static binding between a Widget and a property in a ViewModel. You have to do all data exchange manually.
This sounds like a lot of work, but on the upside it's very straight forward and you have full control over it. Thinking on how often I had problems to debug binding problems it's actually sort of relief.

How to access the ViewModel/AppModel

As Flutter widgets don't have a BindingContext we need another way to access our ViewModel. The easiest way would be just to use a Singleton for this but Flutter has a special Widget for this called InheritedWidget which can be accessed from anywhere of its children. Therefore if we place an InheritedWidget at the very root of our widget tree we can access it from any other Widget during the build process.

This is the InheritedWidget that we will use in our demo App (in model_provider.dart):

// InheritedWidgets allow you to propagate values down the Widget Tree.
// it can then be accessed by just writing  ModelProvider.of(context)
class ModelProvider extends InheritedWidget {
  final AppModel model;

  const ModelProvider({Key key, @required this.model, @required Widget child})
      : assert(model != null),
        assert(child != null),
        super(key: key, child: child);

  static AppModel of(BuildContext context) =>
      (context.inheritFromWidgetOfExactType(ModelProvider) as ModelProvider)
          .model;

  @override
  bool updateShouldNotify(ModelProvider oldWidget) => model != oldWidget.model;
}

It will wrap our ViewModel/AppModel and with the handy static of method we can access it by calling

ModelProvider.of(context)

To make it accessible we put it at the base of our widget tree:

class TheApp extends StatelessWidget {

  final AppModel model;

  const TheApp({Key key, this.model}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return new ModelProvider(
      model: model,
      child: new MaterialApp(
        title: 'Binding Demo',
        home: new MainPage(),
      ),
    );
  }
}

One way binding

Which means we only update the view with data from the App/ViewModel but not vice versa.
For this demo we use a very simple AppModel:


class AppModel { String singleFieldValue = "Just a single Field"; }

To display this field we only have to reference the the ModelProvider in our MainPage's build method (main_page.dart):

  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("Binding Demo")),
      body: 
      Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          children: <Widget>
          [
       -->   Text(ModelProvider.of(context).singleFieldValue),

One nice side effect of that we assign a value on every rebuild is that we can also call any method at this place so for instance to make the value capitalized for displaying:

  Text( ModelProvider.of(context).singleFieldValue.toUpperCase() );

So no need for special converters.

Two way binding

Let's assume we want a TextField to enter a text that should be initialized with a value and update the ViewModel if its value changes:

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text("Binding Demo")),
    body: 
    Padding(
      padding: const EdgeInsets.all(8.0),
      child: Column(
        children: <Widget>
        [
          Text("Single Text field:"),
     -->  TextField(
             controller: TextEditingController(text: ModelProvider.of(context).singleFieldValue,),
     -->     onChanged: ( newValue) => ModelProvider.of(context).singleFieldValue = newValue,
          ),

To initialize a TextField we have to use the TextEditingController of the Widget. To save changes back to our AppModel we have to assign a method to the onChanged handler of the widget which is best done using a lambda method.

Getting dynamic

In this step we will generate several TextFields dynamically from a collection of the AppModel. For that we extend the AppModel:

class FormEntry {
  String title;
  String content;

  FormEntry(this.title,this.content);
}

class AppModel {
    List<FormEntry> formEntries = new List<FormEntry>();

    String singleFieldValue = "Just a single Field";

    AppModel()
    {
      // Fill the List with initial values
      formEntries.addAll(
        [
          FormEntry("Name:","Burkhart"),
          FormEntry("First Name:","Thomas"),
          FormEntry("email:","tom@mydomain.de"),
          FormEntry("Country:","Wakanda"),
        ]);
        printContent();
    }

    void printContent()
    {
       print("Single TextField: $singleFieldValue");
       print("\n");

       formEntries.forEach((entry)=> print("Field: ${entry.title} - Content: ${entry.content}"));
       print(" ");
    }
}

and the build method like that:

Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: Text("Binding Demo")),
    body: 
    Padding(
      padding: const EdgeInsets.all(8.0),
      // creating a column with the single value field and a list of Textfields
      child: Column(
        children: <Widget>
        [
          Text("Single Text field:"),
          TextField(
              controller: TextEditingController(
            text: ModelProvider.of(context).singleFieldValue.toUpperCase(),
          ),

          onChanged: ( newValue) => ModelProvider.of(context).singleFieldValue = newValue,
          ),

          Padding(
            padding: const EdgeInsets.only(top: 50.0, bottom: 10.0),
            child: Container(
              color: Colors.blue,
              height: 3.0,
            ),
          ),

          Text("Multiple Fields:"),

          Expanded(child: 
            ListView.builder(
                itemCount: ModelProvider.of(context).formEntries.length,
                // This builder function is called "formEntries.length" times.
                itemBuilder: (context, index) {
                  return Padding(
                    padding: const EdgeInsets.only(top: 8.0),
                    child: 
                    Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: <Widget>
                      [
                        Text(
                          ModelProvider.of(context).formEntries[index].title,
                          style: TextStyle(fontSize: 20.0),
                        ),
                        TextField(
                          controller: TextEditingController(
                              text: ModelProvider.of(context).formEntries[index].content),
                          // because a new lambda function is created for 
                          // each item, it can capture the current value of index 
                          onChanged: (newValue) => 
                            ModelProvider.of(context).formEntries[index].content = newValue ,
                        )
                      ],
                    ),
                  );
              }),
          ),
          // Will print the current value of the `formEntries`collection in the AppModel
          MaterialButton(
            child: Text("Print"),
            onPressed: ModelProvider.of(context).printContent,
          )
        ],
      ),
    ),
  );

Let's have a closer look at the creation of the TextField:

TextField(
  controller: TextEditingController(text: ModelProvider.of(context).formEntries[index].content),
  onChanged: (newValue) => 
    ModelProvider.of(context).formEntries[index].content = newValue ,
)

The ListView.builder will generate a ListView by calling a provided builder function with the index of the current item. Because we use a lambda function it can capture the passed index through Darts closure feature so that the correct index is used when the handler is called.

You can find the code for this step here: https://github.com/escamoteur/two_way_binding/tree/direct_access

Doing some clean-up

Directly writing to fields in the AppModel is not really a good idea so we add this two methods to the AppModel

updateSingleValueField(String value ) => singleFieldValue = value; 

updateFormEntry(int index, String value) {
  print("Updated Field index: $index: $value");
  return formEntries[index].content = value;
}

You may have already realized if you have run the App in the previous state that when tapping one of the TextField that they are not pushed into view when the keyboard pops up. Because of a current bug this doesn't happen automatically so we have to do a little workaround which consists of theses steps:

  • wrapping the bodyof the Scaffold into a SingleChildScrollView to enable the body to scroll
  • Because ScrollViews don't work without problems with a ListView in it Simon Lightfoot contributed some helper classes:
// Generates a Column dynamically from a a builder method 
class ColumnBuilder extends StatelessWidget {
    final IndexedWidgetBuilder itemBuilder;
    final MainAxisAlignment mainAxisAlignment;
    final MainAxisSize mainAxisSize;
    final CrossAxisAlignment crossAxisAlignment;
    final TextDirection textDirection;
    final VerticalDirection verticalDirection;
    final int itemCount;

    const ColumnBuilder({
        Key key,
        @required this.itemBuilder,
        @required this.itemCount,
        this.mainAxisAlignment: MainAxisAlignment.start,
        this.mainAxisSize: MainAxisSize.max,
        this.crossAxisAlignment: CrossAxisAlignment.center,
        this.textDirection,
        this.verticalDirection: VerticalDirection.down,
    }) : super(key: key);

    @override
    Widget build(BuildContext context) {
        return Column(
            children: new List.generate(this.itemCount,
                    (index) => this.itemBuilder(context, index)).toList(),
        );
    }
}

Which is a nice example of the extensibility of Flutter. The building function now looks like that:

body: new SingleChildScrollView(
        child: Padding(
    padding: const EdgeInsets.all(8.0),
    child: Column(
      children: <Widget>[
        Text("Single Text field:"),
        TextField(
            controller: TextEditingController(
              text: ModelProvider.of(context).singleFieldValue,
            ),
            onChanged: ModelProvider.of(context).updateSingleValueField),
        Padding(
          padding: const EdgeInsets.only(top: 50.0, bottom: 10.0),
          child: Container(
            color: Colors.blue,
            height: 3.0,
          ),
        ),
        Text("Multiple Fields:"),
        ColumnBuilder(
            itemCount: ModelProvider.of(context).formEntries.length,
            itemBuilder: (context, index) {
              return Padding(
                padding: const EdgeInsets.only(top: 8.0),
                child:   
                 EnsureVisible( duration: Duration(milliseconds: 200),
                          ensureVisibleBuilder: (context, focusNode) =>
                    Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: <Widget>[
                        Text(
                          ModelProvider.of(context).formEntries[index].title,
                          style: TextStyle(fontSize: 20.0),
                        ),

                        TextField(focusNode: focusNode,
                          controller: TextEditingController(text: ModelProvider.of(context).formEntries[index].content),
                          // because a new lambda function is created for each item, it can
                          // capture the current value of index
                          onChanged: (newValue) =>
                               ModelProvider.of(context).updateFormEntry(index, newValue),
                        )
                      ],
                ),)
              );
            }),
        MaterialButton(
          child: Text("Print"),
          onPressed: ModelProvider.of(context).printContent,
        )
      ],

EnsureVisible is another helper widget that ensures that its child is scrolled into view as soon as it gets the focus. As this will hopefully be fixed soon by the framework I won't go into details on this.

You can find the code for this step here: https://github.com/escamoteur/two_way_binding/tree/using_functions

If you ran the App and edited one of the fields you may have realized in the debug view that AppModel.updateFormEntry is called after each character that is entered. Often that isn't what we really want. Especially on a page with multiple TextFields it is desired that the data is only updated if all fields are validated and saved together, so that the user can cancel this operation easily, which btw. is an advantage to automatic two way bindings.

Using Forms

This scenario is solved by the Form widget. A Form combines one or more FormFields together so that they can be validated and saved all together.

You can find more on forms validation here

To access the Form from our Button we have to use a GlobalKey because its not in the same tree branch as the Form otherwise we could look it up with context.ancestorWidgetOfExactType.

To access the keys easily we put them into a class as static fields (app_keys.dart):

class AppKeys {
  static final GlobalKey form = new GlobalKey();
}

With a Form it now looks like this:

Text("Multiple Fields:"),

new Form(key: AppKeys.form,
      child: 
      ColumnBuilder(
          itemCount: ModelProvider.of(context).formEntries.length,
          itemBuilder: (context, index) 
          {
              return Padding(
                padding: const EdgeInsets.only(top: 8.0),
                child:    
                EnsureVisible(duration: Duration(milliseconds: 200), ensureVisibleBuilder: (context, focusNode) =>
                  Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>
                    [
                      Text(ModelProvider.of(context).formEntries[index].title,
                            style: TextStyle(fontSize: 20.0),
                            ),

                      TextFormField(
                          focusNode: focusNode,
                          controller: TextEditingController(text: ModelProvider.of(context).formEntries[index].content),
                          // because a new lambda function is created for 
                          // each item, it can capture the current value of index
                          onSaved: (newValue) => 
                              ModelProvider.of(context).updateFormEntry(index, newValue),
                    ],
                  ),
                )
              );
            }),
),
MaterialButton(
  child: Text("Print"),
  onPressed: () 
        { 
            FormState state = AppKeys.form.currentState;
            state.save();
            ModelProvider.of(context).printContent();
        }
)

You will have realized that a TextFormField has a slightly different constructor:

TextFormField(ontroller: new TextEditingController(text: ModelProvider.of(context).formEntries[index].content),
    focusNode: focusNode,
    onSaved: (newValue) => ModelProvider.of(context).updateFormEntry(index, newValue),
    )

The event handler is now called onSave which means it will be called if the parent FormState's save method is called.
This is a bit counterintuitive that we have to access the FormState and not the Form itself but because the Form doesn't keep any state it's not able to hold the save logic.

Luckily if you assign any widget a GlobalKey you can access the widget, the widget's state if it's a StatefullWidget and the widget's context over the GlobalKey like on the button:

MaterialButton(
  child: Text("Print"),
  onPressed: () 
        { 
            FormState state = AppKeys.form.currentState;
            state.save();
            ModelProvider.of(context).printContent();
        }
)

You can find the code for this step here: https://github.com/escamoteur/two_way_binding/tree/using_form_fields

Updating widgets

So far we only transferred values from the AppModel to the page on the first build of the page. If we change any values inside our AppModel this won't be reflected in the view because it won't trigger any rebuild, which is actually a good thing compared to automatic bindings that would trigger updates on any data change. The downside is we have to manually trigger the rebuild of our screen. To show this I add a second button that will change the value of our ViewModel. There are several ways we can trigger an update:

The call-back approach

One way to deal with that is that we change our MainPage from a StatelessWidgetto a StatefulWidget so that we can register a call back in the states didChangeDependencies method:

class MainPage extends StatefulWidget {

  @override
  MainPageState createState() {
    return new MainPageState();
  }
}

class MainPageState extends State<MainPage> {

  @override
  void didChangeDependencies() {
      ModelProvider.of(context).addListener(() => setState((){}));
      super.didChangeDependencies();
    }

  @override
  Widget build(BuildContext context) {
    return Scaffold(

We cannot register the call-back in initState because the context isn't available at that time.

To make this work we inherit the AppModel now from ChangeNotifier which allows to register listeners and raise notifications to that listeners.

Calling the setState in the call-back will trigger a full rebuild of the States child.

I also added a print output that will show if the whole Page was rebuild:

Column(children: <Widget>
[
  Text(() {print("************** Was updated****"); return "Single Text field:";}()  ),

Using a lambda like this is a handy way to debug the build process of the widget tree

If you start this version of the App and hit Update you will see this message as second time after start-up which means we have updated the whole page although we just change the content of our list of FormFields.

You can find the code for this step here: https://github.com/escamoteur/two_way_binding/tree/update_with_call_back

The stream approach

If you have read my blog post on reactive Flutter you know that I'm a big friend of Streams and StreamBuilders which rebuild their children as soon as they receive a new item from a Stream. So this is a very elegant approach to make your View update when your data changes.

To make that work we add a StreamController to the AppModel which will create a Stream to which we can push events:

class AppModel {
    List<FormEntry> formEntries = new List<FormEntry>();
    StreamController<List<FormEntry>> listUpdates = new StreamController<List<FormEntry>>(); 

    ....

    changeAppModel()
    {
      formEntries.clear();
      formEntries.addAll(
        [
          FormEntry("Name:","New Name"),
          FormEntry("First Name:","New First Name"),
          FormEntry("email:","New email"),
          FormEntry("Country:","New Country"),
        ]);

  -->   listUpdates.add(formEntries); // This will trigger the update
    }

And in the MainPage:

Text("Multiple Fields:"),

new Form(key: AppKeys.form,
      child: 
      new StreamBuilder<List<FormEntry>>(
            stream: ModelProvider.of(context).listUpdates.stream,
            initialData: ModelProvider.of(context).formEntries,
            builder: (context, snapShot)
            {
              List<FormEntry> list = snapShot.data;
              return ColumnBuilder(
                    itemCount: ModelProvider.of(context).formEntries.length,
                    itemBuilder: (context, index) 
                    {
                        return Padding(
                          padding: const EdgeInsets.only(top: 8.0),
                          child:    
                          EnsureVisible(duration: Duration(milliseconds: 200), ensureVisibleBuilder: (context, focusNode) =>
                            Column(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: <Widget>
                              [
                                Text(list[index].title,
                                    style: TextStyle(fontSize: 20.0),
                                    ),

                                TextFormField(controller: new TextEditingController(text:list[index].content),
                                    focusNode: focusNode,
                                    // because a new lambda function is created for each item, 
                                    // it can capture the current value of index
                                    onSaved: (newValue) => ModelProvider.of(context).updateFormEntry(index, newValue),
                                    )
                              ],
                            ),
                          )
                        );

If you run this App you will see that only the Widgets below the StreamBuilder are updated.

You can find the code for this step here: https://github.com/escamoteur/two_way_binding/tree/using_stream

Contact me:

8 thoughts on “Flutter: Bindings? What bindings?

  1. Caliaro Alessandro says:

    Wakanda?

  2. Andy Wong says:

    Bindings in Flutter’s Widgets Tree,
    can be found, everywhere!
    Using Properties, for conditional Rendering,
    Are Everywhere,
    This is what,
    Make Flutter-Dart, Interesting.
    isThisProperty ? New Container() : buildNewUI,
    If, the property, were not initialized, yet,
    It render a blank, til, it does.

    • admin says:

      But this isn’t what you normally understand when talking about bindings. That’s just the fact that you directly can access your code while building the tree, which I explained in the posed I thought.

  3. Andy Wong says:

    Binding a tree, like IBAction,IBoutlet as in XCODE binding.
    Binding under XAML, means same as binding to tree as in Flutter,
    May be you are not using the term correctly,
    You may bind a data object to a widget,
    just like, use a converter, under UWP xaml, 1 way or 2 ways,
    under XAML or Flutter, can be same,
    You need, only a little coding technique,
    Just like you are employing above.
    You are, into MVVM, this are good but old methods.
    Future Builder to load data to Widget, widget Handler may be used to refresh the data.

    • admin says:

      What I described in my post is not what is called bindings on other Platforms. But I wanted to make it easier for people coming fro other platforms to understand how this works in Flutter. If you have seen my other posts you know that I’m in favor of an Rx approach to update widgets.

  4. Mike says:

    Maybe it’s just me, but coming from a XAML world, this looks very alien and looks very iOS-ish.

    Alot of UI creation done in code mixed with handling the events.

    Is it just me or it really does not have the clarity and simplicity of XAML?

    Xamarin Forms is poorly implemented by Xamarin by lack of resources and dedication, but XAML as a technology it’s simpler.
    Again, maybe it’s just me.

    • Thomas Burkhart says:

      I understand what you mean, because I too came from Xamarin. It takes a bit to get used to it but once you understood the reactive pattern that rebuilds a tree in code you never want to go back. It’s such more powerful to be able to use loops and ifs while building your Views.

Leave a Reply to Andy Wong Cancel reply

Your email address will not be published. Required fields are marked *